Skip to content

Commit a78c117

Browse files
committed
fix(aws-marketplace): treat a usage-only read as no answer about access, and refuse to boot on a malformed product catalog
1 parent 35e9a84 commit a78c117

7 files changed

Lines changed: 23 additions & 15 deletions

File tree

‎apps/sim/bootstrap.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
/**
2-
* Container entrypoint. Hydrates `process.env` from the runtime secret, resolves an AWS
3-
* Marketplace Enterprise license into `ENTERPRISE_ENABLED` on Marketplace installs, and drops
2+
* Container entrypoint. Hydrates `process.env` from the runtime secret, refuses to start on a
3+
* malformed `AWS_MARKETPLACE_PRODUCTS` (so a bad deploy rolls back instead of failing buyers and
4+
* checkouts later), resolves an AWS Marketplace Enterprise license into `ENTERPRISE_ENABLED` on
5+
* Marketplace installs, and drops
46
* Next's route cache when an earlier build wrote it, before loading the Next.js standalone server,
57
* so application modules that read env at import time see the full configuration. See
68
* `@sim/runtime-secrets`.
79
*/
810
import { fileURLToPath } from 'node:url'
911
import { loadRuntimeSecrets } from '@sim/runtime-secrets'
1012
import { applyAwsMarketplaceContainerLicense } from '@/lib/billing/aws-marketplace/container-license'
13+
import { parseAwsMarketplaceProducts } from '@/lib/billing/aws-marketplace/products'
1114
import { clearRouteCacheFromOtherBuild } from '@/lib/core/utils/route-cache'
1215

1316
await loadRuntimeSecrets()
17+
if (process.env.AWS_MARKETPLACE_PRODUCTS) {
18+
parseAwsMarketplaceProducts(process.env.AWS_MARKETPLACE_PRODUCTS)
19+
}
1420
await applyAwsMarketplaceContainerLicense()
1521
await clearRouteCacheFromOtherBuild(fileURLToPath(new URL('./.next', import.meta.url)))
1622
// `server.js` is the Next standalone build artifact, a sibling of this file in

‎apps/sim/lib/billing/aws-marketplace/config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ let products: ReadonlyMap<string, AwsMarketplaceProduct> | undefined
99
/**
1010
* The products this deployment sells through AWS Marketplace, by product code. Empty on
1111
* self-hosted installs and every environment but production, which turns the Marketplace surfaces
12-
* off. A malformed setting throws a message naming the bad entry.
12+
* off. The container entrypoint refuses to start on a malformed setting.
1313
*/
1414
export function getAwsMarketplaceProducts(): ReadonlyMap<string, AwsMarketplaceProduct> {
1515
products ??= env.AWS_MARKETPLACE_PRODUCTS

‎apps/sim/lib/billing/aws-marketplace/entitlements.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ describe('verifyLicense', () => {
104104
expect(summary).toMatchObject({ active: true, dimension: 'b-current', quantity: 50 })
105105
})
106106

107-
it('does not let a current usage entitlement stand in for a required platform one', async () => {
107+
it('reads a usage entitlement alone as no answer about access, not as access', async () => {
108108
const summary = await readAs(USAGE, entitlement({ Dimension: 'llm_credits' }))
109-
expect(summary).toMatchObject({ active: false, entitlementCount: 1, dimension: null })
109+
expect(summary).toMatchObject({ active: false, entitlementCount: 0, dimension: null })
110110
})
111111

112112
it('reads a lapsed platform as inactive even while usage is current', async () => {
@@ -115,7 +115,7 @@ describe('verifyLicense', () => {
115115
entitlement({ Dimension: 'llm_credits' }),
116116
entitlement({ Dimension: 'platform', ExpirationDate: new Date('2026-01-01T00:00:00.000Z') })
117117
)
118-
expect(summary).toMatchObject({ active: false, entitlementCount: 2, dimension: 'platform' })
118+
expect(summary).toMatchObject({ active: false, entitlementCount: 1, dimension: 'platform' })
119119
})
120120

121121
it('reads a current platform as active and describes it, not the usage dimension', async () => {

‎apps/sim/lib/billing/aws-marketplace/entitlements.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ export interface EntitlementSummary {
1717
*/
1818
active: boolean
1919
/**
20-
* Entitlements AWS returned for any dimension, current or lapsed: whether AWS has answered at all.
21-
* Zero is ambiguous, see the license's status.
20+
* Entitlements AWS returned for the dimension that grants access (any dimension, for a plan with
21+
* none), current or lapsed: whether AWS has answered about access at all. A usage dimension alone
22+
* is no answer. Zero is ambiguous, see the license's status.
2223
*/
2324
entitlementCount: number
2425
dimension: string | null
@@ -57,7 +58,7 @@ function summarizeEntitlements(
5758
const chosen = current[0] ?? lapsed[0]
5859
return {
5960
active: current.length > 0,
60-
entitlementCount: allEntitlements.length,
61+
entitlementCount: entitlements.length,
6162
dimension: chosen?.Dimension ?? null,
6263
quantity: chosen?.Value?.IntegerValue ?? null,
6364
expiresAt: chosen?.ExpirationDate ?? null,

‎apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,13 @@ describe('POST /api/marketplace/aws/register', () => {
405405
currentEntitlement({ ProductCode: USAGE_PRODUCT, Dimension: 'llm_credits' }),
406406
])
407407

408-
const refused = await registration('x-amzn-marketplace-token=usage-only-token')
408+
const unconfirmed = await registration('x-amzn-marketplace-token=usage-only-token')
409409

410-
expect(errorOf(refused)).toBe('no-subscription')
410+
expect(claimTokenOf(unconfirmed)).not.toBeNull()
411+
expect(entitlementSend).toHaveBeenCalledTimes(3)
411412
expect(await readLicense(usageOnly)).toMatchObject({
412413
productCode: USAGE_PRODUCT,
413-
status: 'inactive',
414+
status: 'pending',
414415
})
415416

416417
const withPlatform = uniqueArn()

‎apps/sim/lib/billing/aws-marketplace/products.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
/** How one listing is sold, and so what Sim requires before granting access. */
8-
export interface AwsMarketplacePlan {
8+
interface AwsMarketplacePlan {
99
readonly id: string
1010
/**
1111
* The entitlement dimension that must be current for a license to count as active. Null accepts
@@ -21,7 +21,7 @@ const PLANS = [
2121
{ id: 'platform-with-usage', requiredDimension: 'platform' },
2222
] as const satisfies readonly AwsMarketplacePlan[]
2323

24-
export const AWS_MARKETPLACE_PLANS: ReadonlyMap<string, AwsMarketplacePlan> = new Map(
24+
const AWS_MARKETPLACE_PLANS: ReadonlyMap<string, AwsMarketplacePlan> = new Map(
2525
PLANS.map((plan) => [plan.id, plan])
2626
)
2727

‎apps/sim/lib/billing/aws-marketplace/registration.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function errorName(error: unknown): string | undefined {
2929
return error instanceof Error ? error.name : undefined
3030
}
3131

32-
/** AWS may not have finished right after a purchase, so an empty answer is asked again briefly. */
32+
/** AWS may not have finished right after a purchase, so a read with no answer about access is asked again briefly. */
3333
async function readEntitlements(
3434
product: AwsMarketplaceProduct,
3535
licenseArn: string

0 commit comments

Comments
 (0)