@farthershore/product 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/dist/bin.js +457 -75
- package/dist/codegen.js +23 -0
- package/dist/index.js +418 -70
- package/dist/types/business.d.ts +35 -10
- package/dist/types/index.d.ts +2 -1
- package/dist/types/ir-types.d.ts +25 -0
- package/dist/types/resource-graph.d.ts +36 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,97 @@ var ManifestBuilderError = class extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
// src/resource-graph.ts
|
|
24
|
+
var ManifestResourceGraph = class {
|
|
25
|
+
nodes = /* @__PURE__ */ new Map();
|
|
26
|
+
declarationOrder = 0;
|
|
27
|
+
register(kind, key, value, dependsOn = []) {
|
|
28
|
+
const id = nodeId(kind, key);
|
|
29
|
+
if (this.nodes.has(id)) {
|
|
30
|
+
throw new Error(`duplicate resource node ${id}`);
|
|
31
|
+
}
|
|
32
|
+
const node = {
|
|
33
|
+
urn: resourceUrn(kind, key),
|
|
34
|
+
kind,
|
|
35
|
+
key,
|
|
36
|
+
value,
|
|
37
|
+
dependsOn: [...new Set(dependsOn)],
|
|
38
|
+
declarationOrder: this.declarationOrder
|
|
39
|
+
};
|
|
40
|
+
this.declarationOrder += 1;
|
|
41
|
+
this.nodes.set(id, node);
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
upsert(kind, key, value, dependsOn = []) {
|
|
45
|
+
const id = nodeId(kind, key);
|
|
46
|
+
const existing = this.nodes.get(id);
|
|
47
|
+
if (!existing) {
|
|
48
|
+
return this.register(kind, key, value, dependsOn);
|
|
49
|
+
}
|
|
50
|
+
const node = {
|
|
51
|
+
urn: existing.urn,
|
|
52
|
+
kind,
|
|
53
|
+
key,
|
|
54
|
+
value,
|
|
55
|
+
dependsOn: [...new Set(dependsOn)],
|
|
56
|
+
declarationOrder: existing.declarationOrder
|
|
57
|
+
};
|
|
58
|
+
this.nodes.set(id, node);
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
clearKind(kind) {
|
|
62
|
+
for (const [id, node] of this.nodes.entries()) {
|
|
63
|
+
if (node.kind === kind) this.nodes.delete(id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
has(kind, key) {
|
|
67
|
+
return this.nodes.has(nodeId(kind, key));
|
|
68
|
+
}
|
|
69
|
+
get(kind, key) {
|
|
70
|
+
return this.nodes.get(nodeId(kind, key)) ?? null;
|
|
71
|
+
}
|
|
72
|
+
values(kind) {
|
|
73
|
+
return [...this.nodes.values()].filter((node) => node.kind === kind).map((node) => node.value);
|
|
74
|
+
}
|
|
75
|
+
sortedValues(kind, key) {
|
|
76
|
+
return sortBy(this.values(kind), key);
|
|
77
|
+
}
|
|
78
|
+
snapshot() {
|
|
79
|
+
return {
|
|
80
|
+
nodes: sortBy([...this.nodes.values()], (node) => node.urn).map(
|
|
81
|
+
({ value: _value, ...node }) => node
|
|
82
|
+
)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
missingDependencies() {
|
|
86
|
+
const known = new Set([...this.nodes.values()].map((node) => node.urn));
|
|
87
|
+
return [...this.nodes.values()].flatMap(
|
|
88
|
+
(node) => node.dependsOn.filter((dependency) => !known.has(dependency)).map((dependency) => ({
|
|
89
|
+
from: node.urn,
|
|
90
|
+
missing: dependency
|
|
91
|
+
}))
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
function resourceUrn(kind, key) {
|
|
96
|
+
return `urn:farthershore:product:${kind}:${encodeURIComponent(
|
|
97
|
+
key
|
|
98
|
+
)}`;
|
|
99
|
+
}
|
|
100
|
+
function resourceDependency(kind, key) {
|
|
101
|
+
return resourceUrn(kind, key);
|
|
102
|
+
}
|
|
103
|
+
function nodeId(kind, key) {
|
|
104
|
+
return `${kind}:${key}`;
|
|
105
|
+
}
|
|
106
|
+
function sortBy(items, key) {
|
|
107
|
+
return [...items].sort((a, b) => {
|
|
108
|
+
const ka = key(a);
|
|
109
|
+
const kb = key(b);
|
|
110
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
23
114
|
// ../contracts/dist/plans/limits-schema.js
|
|
24
115
|
import { z } from "zod";
|
|
25
116
|
var limitDimensionSchema = z.string().min(1);
|
|
@@ -1244,6 +1335,77 @@ var featureCatalogEntrySchema = z13.object({
|
|
|
1244
1335
|
plans: z13.array(z13.string().min(1)).min(1).max(20)
|
|
1245
1336
|
});
|
|
1246
1337
|
var featureCatalogSchema = z13.record(z13.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
|
|
1338
|
+
var productCleanupPolicyModeSchema = z13.enum([
|
|
1339
|
+
"report",
|
|
1340
|
+
"pull_request"
|
|
1341
|
+
]);
|
|
1342
|
+
var productChangeApprovalRiskSchema = z13.enum([
|
|
1343
|
+
"safe",
|
|
1344
|
+
"non_blocking",
|
|
1345
|
+
"economic_risk",
|
|
1346
|
+
"blocking"
|
|
1347
|
+
]);
|
|
1348
|
+
var productOperatorPoliciesSchema = z13.object({
|
|
1349
|
+
/**
|
|
1350
|
+
* Route cleanup operator. Disabled by default; report-mode is the safe
|
|
1351
|
+
* default so a product can surface zero-traffic runtime-route candidates
|
|
1352
|
+
* before it opts into draft PR mutation.
|
|
1353
|
+
*/
|
|
1354
|
+
cleanup: z13.object({
|
|
1355
|
+
enabled: z13.boolean().default(false),
|
|
1356
|
+
mode: productCleanupPolicyModeSchema.default("report")
|
|
1357
|
+
}).default({ enabled: false, mode: "report" }),
|
|
1358
|
+
/**
|
|
1359
|
+
* PR approval thresholds for manifest impact reports. This schema makes
|
|
1360
|
+
* the policy a first-class product-as-code field even while enforcement is
|
|
1361
|
+
* still report/label-only.
|
|
1362
|
+
*/
|
|
1363
|
+
change_approval: z13.object({
|
|
1364
|
+
auto_merge_max_risk: z13.enum(["none", "safe", "non_blocking"]).default("none"),
|
|
1365
|
+
require_human_for: z13.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
|
|
1366
|
+
}).default({
|
|
1367
|
+
auto_merge_max_risk: "none",
|
|
1368
|
+
require_human_for: ["economic_risk", "blocking"]
|
|
1369
|
+
})
|
|
1370
|
+
}).default({
|
|
1371
|
+
cleanup: { enabled: false, mode: "report" },
|
|
1372
|
+
change_approval: {
|
|
1373
|
+
auto_merge_max_risk: "none",
|
|
1374
|
+
require_human_for: ["economic_risk", "blocking"]
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
var customerIdentityRequirementSchema = z13.enum([
|
|
1378
|
+
"org_only",
|
|
1379
|
+
"org_and_user"
|
|
1380
|
+
]);
|
|
1381
|
+
var customerPortalAuthStrategySchema = z13.enum([
|
|
1382
|
+
"clerk",
|
|
1383
|
+
"test-personas"
|
|
1384
|
+
]);
|
|
1385
|
+
var productCustomerContextSchema = z13.object({
|
|
1386
|
+
/**
|
|
1387
|
+
* Edge credential identity policy. This is intentionally Product-scoped:
|
|
1388
|
+
* B7 keeps Product as the business boundary and avoids customer-side
|
|
1389
|
+
* Workspace/subject models until per-subject entitlements need them.
|
|
1390
|
+
*/
|
|
1391
|
+
identity_requirement: customerIdentityRequirementSchema.optional(),
|
|
1392
|
+
/**
|
|
1393
|
+
* Enables signed customer-context tokens (`fsc_*`) without putting the
|
|
1394
|
+
* runtime signing secret in product/product.config.ts. Core generates/preserves the
|
|
1395
|
+
* secret when this is true and clears it when explicitly false.
|
|
1396
|
+
*/
|
|
1397
|
+
context_tokens: z13.object({
|
|
1398
|
+
enabled: z13.boolean().default(true)
|
|
1399
|
+
}).optional(),
|
|
1400
|
+
/**
|
|
1401
|
+
* Portal auth strategy for environment-scoped product applies. Production
|
|
1402
|
+
* portal auth is provisioner-owned; preview/test environments can opt into
|
|
1403
|
+
* test personas through Product-as-Code.
|
|
1404
|
+
*/
|
|
1405
|
+
portal_auth: z13.object({
|
|
1406
|
+
strategy: customerPortalAuthStrategySchema
|
|
1407
|
+
}).optional()
|
|
1408
|
+
});
|
|
1247
1409
|
var productSpecSchema = z13.object({
|
|
1248
1410
|
product: z13.object({
|
|
1249
1411
|
name: z13.string().min(1).max(100),
|
|
@@ -1275,11 +1437,12 @@ var productSpecSchema = z13.object({
|
|
|
1275
1437
|
}).optional(),
|
|
1276
1438
|
features: featureCatalogSchema.optional(),
|
|
1277
1439
|
resources: countedResourcesSchema,
|
|
1440
|
+
policies: productOperatorPoliciesSchema,
|
|
1441
|
+
customer_context: productCustomerContextSchema.optional(),
|
|
1278
1442
|
/**
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
* sections remain template-owned.
|
|
1443
|
+
* Legacy/internal declarative frontend surface. New product repos customize
|
|
1444
|
+
* the generated `frontend/` project directly; this remains for existing IR
|
|
1445
|
+
* and internal template metadata.
|
|
1283
1446
|
*/
|
|
1284
1447
|
frontend: frontendManifestSchema.optional(),
|
|
1285
1448
|
migrations: migrationDeclsSchema.optional(),
|
|
@@ -1403,7 +1566,7 @@ var productSpecSchema = z13.object({
|
|
|
1403
1566
|
*
|
|
1404
1567
|
* Once a product has compiled with a `webhooks` block, API mutations
|
|
1405
1568
|
* on those endpoints fail with `409 MANAGED_BY_CODE` — see
|
|
1406
|
-
* `core/src/routes/management-webhooks.ts`. Tie-breaker: product.config.ts
|
|
1569
|
+
* `core/src/routes/management-webhooks.ts`. Tie-breaker: product/product.config.ts
|
|
1407
1570
|
* wins over API state if an endpoint id appears in both.
|
|
1408
1571
|
*/
|
|
1409
1572
|
webhooks: webhooksBlockSchema.optional(),
|
|
@@ -1990,6 +2153,8 @@ var productSpecV2Schema = z20.object({
|
|
|
1990
2153
|
frontend: frontendManifestSchema.optional(),
|
|
1991
2154
|
migrations: migrationDeclsSchema.optional(),
|
|
1992
2155
|
resources: countedResourcesSchema,
|
|
2156
|
+
policies: productOperatorPoliciesSchema,
|
|
2157
|
+
customer_context: productCustomerContextSchema.optional(),
|
|
1993
2158
|
billing: z20.object({
|
|
1994
2159
|
gracePeriodDays: z20.number().int().nonnegative().default(3),
|
|
1995
2160
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
@@ -2190,7 +2355,7 @@ function canonicalIrJson(ir) {
|
|
|
2190
2355
|
}
|
|
2191
2356
|
|
|
2192
2357
|
// src/version.ts
|
|
2193
|
-
var SDK_VERSION = true ? "0.
|
|
2358
|
+
var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
2194
2359
|
|
|
2195
2360
|
// src/business.ts
|
|
2196
2361
|
var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
|
|
@@ -2239,13 +2404,7 @@ var Business = class {
|
|
|
2239
2404
|
[BUSINESS_BRAND] = true;
|
|
2240
2405
|
name;
|
|
2241
2406
|
options;
|
|
2242
|
-
|
|
2243
|
-
resources = /* @__PURE__ */ new Map();
|
|
2244
|
-
plans = /* @__PURE__ */ new Map();
|
|
2245
|
-
features = /* @__PURE__ */ new Map();
|
|
2246
|
-
policies = /* @__PURE__ */ new Map();
|
|
2247
|
-
capabilities = /* @__PURE__ */ new Map();
|
|
2248
|
-
migrations = [];
|
|
2407
|
+
graph = new ManifestResourceGraph();
|
|
2249
2408
|
frontendManifest;
|
|
2250
2409
|
productPatch = {};
|
|
2251
2410
|
/** Sugar for binding API routes to features. */
|
|
@@ -2265,16 +2424,21 @@ var Business = class {
|
|
|
2265
2424
|
}
|
|
2266
2425
|
this.name = name;
|
|
2267
2426
|
this.options = options;
|
|
2427
|
+
this.graph.register("product", name, {
|
|
2428
|
+
name,
|
|
2429
|
+
options
|
|
2430
|
+
});
|
|
2268
2431
|
this.api = {
|
|
2269
2432
|
route: (match, options2) => {
|
|
2270
2433
|
const featureKey = keyOf(options2.feature);
|
|
2271
|
-
const file = this.
|
|
2434
|
+
const file = this.getFeatureFile(featureKey);
|
|
2272
2435
|
if (!file) {
|
|
2273
2436
|
throw new ManifestBuilderError(
|
|
2274
2437
|
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call business.feature("${featureKey}", \u2026) first`
|
|
2275
2438
|
);
|
|
2276
2439
|
}
|
|
2277
2440
|
file.routes.push(this.buildRoute(match, options2));
|
|
2441
|
+
this.syncFeatureGraphNode(file);
|
|
2278
2442
|
return this;
|
|
2279
2443
|
}
|
|
2280
2444
|
};
|
|
@@ -2286,6 +2450,7 @@ var Business = class {
|
|
|
2286
2450
|
path: item.path,
|
|
2287
2451
|
...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
|
|
2288
2452
|
}));
|
|
2453
|
+
this.syncFrontendGraphNode(manifest);
|
|
2289
2454
|
return this;
|
|
2290
2455
|
},
|
|
2291
2456
|
page: (path, options2) => {
|
|
@@ -2310,17 +2475,19 @@ var Business = class {
|
|
|
2310
2475
|
}))
|
|
2311
2476
|
} : {}
|
|
2312
2477
|
});
|
|
2478
|
+
this.syncFrontendGraphNode(manifest);
|
|
2313
2479
|
return this;
|
|
2314
2480
|
},
|
|
2315
2481
|
manifest: (manifest) => {
|
|
2316
2482
|
this.frontendManifest = manifest;
|
|
2483
|
+
this.syncFrontendGraphNode(manifest);
|
|
2317
2484
|
return this;
|
|
2318
2485
|
}
|
|
2319
2486
|
};
|
|
2320
2487
|
this.lifecycle = {
|
|
2321
2488
|
migration: (id, options2) => {
|
|
2322
|
-
this.
|
|
2323
|
-
|
|
2489
|
+
this.assertNewKey("lifecycle_migration", id, "migration");
|
|
2490
|
+
const migration = {
|
|
2324
2491
|
id,
|
|
2325
2492
|
from: this.normalizeMigrationPlanRef(options2.from),
|
|
2326
2493
|
to: this.normalizeMigrationTargetRef(options2.to),
|
|
@@ -2337,12 +2504,26 @@ var Business = class {
|
|
|
2337
2504
|
...pin.notes !== void 0 ? { notes: pin.notes } : {}
|
|
2338
2505
|
}))
|
|
2339
2506
|
} : {}
|
|
2340
|
-
}
|
|
2507
|
+
};
|
|
2508
|
+
this.graph.register(
|
|
2509
|
+
"lifecycle_migration",
|
|
2510
|
+
id,
|
|
2511
|
+
migration,
|
|
2512
|
+
this.migrationDependsOn(migration)
|
|
2513
|
+
);
|
|
2341
2514
|
return this;
|
|
2342
2515
|
},
|
|
2343
2516
|
migrations: (migrations) => {
|
|
2344
2517
|
this.assertUniqueMigrationIds(migrations);
|
|
2345
|
-
this.
|
|
2518
|
+
this.graph.clearKind("lifecycle_migration");
|
|
2519
|
+
for (const migration of migrations) {
|
|
2520
|
+
this.graph.register(
|
|
2521
|
+
"lifecycle_migration",
|
|
2522
|
+
migration.id,
|
|
2523
|
+
migration,
|
|
2524
|
+
this.migrationDependsOn(migration)
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2346
2527
|
return this;
|
|
2347
2528
|
}
|
|
2348
2529
|
};
|
|
@@ -2352,43 +2533,44 @@ var Business = class {
|
|
|
2352
2533
|
return this;
|
|
2353
2534
|
},
|
|
2354
2535
|
plan: (spec) => {
|
|
2355
|
-
this.assertNewKey(
|
|
2356
|
-
this.
|
|
2536
|
+
this.assertNewKey("plan", spec.key, "plan");
|
|
2537
|
+
this.graph.register("plan", spec.key, spec, this.planDependsOn(spec));
|
|
2357
2538
|
return this;
|
|
2358
2539
|
},
|
|
2359
2540
|
routesFile: (file) => {
|
|
2360
|
-
this.assertNewKey(
|
|
2361
|
-
this.
|
|
2541
|
+
this.assertNewKey("feature", file.feature, "feature");
|
|
2542
|
+
this.registerFeatureFile(file);
|
|
2362
2543
|
return this;
|
|
2363
2544
|
},
|
|
2364
2545
|
policyFile: (file) => {
|
|
2365
|
-
this.assertNewKey(
|
|
2366
|
-
this.
|
|
2546
|
+
this.assertNewKey("policy", file.name, "policy");
|
|
2547
|
+
this.graph.register("policy", file.name, file);
|
|
2367
2548
|
return this;
|
|
2368
2549
|
},
|
|
2369
2550
|
capabilityFile: (file) => {
|
|
2370
|
-
this.assertNewKey(
|
|
2371
|
-
this.
|
|
2551
|
+
this.assertNewKey("capability", file.capability, "capability");
|
|
2552
|
+
this.graph.register("capability", file.capability, file);
|
|
2372
2553
|
return this;
|
|
2373
2554
|
},
|
|
2374
2555
|
frontend: (manifest) => {
|
|
2375
2556
|
this.frontendManifest = manifest;
|
|
2557
|
+
this.syncFrontendGraphNode(manifest);
|
|
2376
2558
|
return this;
|
|
2377
2559
|
}
|
|
2378
2560
|
};
|
|
2379
2561
|
}
|
|
2380
2562
|
meter(key, options) {
|
|
2381
|
-
this.assertNewKey(
|
|
2382
|
-
this.
|
|
2563
|
+
this.assertNewKey("meter", key, "meter");
|
|
2564
|
+
this.graph.register("meter", key, { key, ...options });
|
|
2383
2565
|
return { kind: "meter", key };
|
|
2384
2566
|
}
|
|
2385
2567
|
resource(name, options = {}) {
|
|
2386
|
-
this.assertNewKey(
|
|
2387
|
-
this.
|
|
2568
|
+
this.assertNewKey("counted_resource", name, "resource");
|
|
2569
|
+
this.graph.register("counted_resource", name, { name, ...options });
|
|
2388
2570
|
return { kind: "resource", key: name };
|
|
2389
2571
|
}
|
|
2390
2572
|
capability(key, options = {}) {
|
|
2391
|
-
this.assertNewKey(
|
|
2573
|
+
this.assertNewKey("capability", key, "capability");
|
|
2392
2574
|
const file = {
|
|
2393
2575
|
capability: key,
|
|
2394
2576
|
...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
|
|
@@ -2397,7 +2579,12 @@ var Business = class {
|
|
|
2397
2579
|
...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
|
|
2398
2580
|
...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
|
|
2399
2581
|
};
|
|
2400
|
-
this.
|
|
2582
|
+
this.graph.register(
|
|
2583
|
+
"capability",
|
|
2584
|
+
key,
|
|
2585
|
+
file,
|
|
2586
|
+
this.capabilityDependsOn(file)
|
|
2587
|
+
);
|
|
2401
2588
|
return {
|
|
2402
2589
|
kind: "capability",
|
|
2403
2590
|
key,
|
|
@@ -2409,7 +2596,7 @@ var Business = class {
|
|
|
2409
2596
|
};
|
|
2410
2597
|
}
|
|
2411
2598
|
feature(key, options = {}) {
|
|
2412
|
-
this.assertNewKey(
|
|
2599
|
+
this.assertNewKey("feature", key, "feature");
|
|
2413
2600
|
const file = {
|
|
2414
2601
|
feature: key,
|
|
2415
2602
|
routes: [],
|
|
@@ -2436,30 +2623,34 @@ var Business = class {
|
|
|
2436
2623
|
for (const route of options.routes ?? []) {
|
|
2437
2624
|
file.routes.push(this.buildRoute(route.match, route));
|
|
2438
2625
|
}
|
|
2439
|
-
this.
|
|
2626
|
+
this.registerFeatureFile(file);
|
|
2440
2627
|
const ref = {
|
|
2441
2628
|
kind: "feature",
|
|
2442
2629
|
key,
|
|
2443
2630
|
action: (id, actionOptions) => {
|
|
2444
2631
|
file.actions ??= [];
|
|
2445
|
-
if (file.actions.some((
|
|
2632
|
+
if (file.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
|
|
2446
2633
|
throw new ManifestBuilderError(
|
|
2447
2634
|
`duplicate action "${id}" \u2014 each action id must be declared once`
|
|
2448
2635
|
);
|
|
2449
2636
|
}
|
|
2450
|
-
|
|
2637
|
+
const action = { id, ...actionOptions };
|
|
2638
|
+
file.actions.push(action);
|
|
2639
|
+
this.registerAction(key, action);
|
|
2640
|
+
this.syncFeatureGraphNode(file);
|
|
2451
2641
|
return { kind: "action", key: id };
|
|
2452
2642
|
},
|
|
2453
2643
|
route: (match, routeOptions) => {
|
|
2454
2644
|
file.routes.push(this.buildRoute(match, routeOptions ?? {}));
|
|
2645
|
+
this.syncFeatureGraphNode(file);
|
|
2455
2646
|
return ref;
|
|
2456
2647
|
}
|
|
2457
2648
|
};
|
|
2458
2649
|
return ref;
|
|
2459
2650
|
}
|
|
2460
2651
|
policy(name, options) {
|
|
2461
|
-
this.assertNewKey(
|
|
2462
|
-
|
|
2652
|
+
this.assertNewKey("policy", name, "policy");
|
|
2653
|
+
const file = {
|
|
2463
2654
|
name,
|
|
2464
2655
|
type: options.type,
|
|
2465
2656
|
config: options.config,
|
|
@@ -2473,11 +2664,12 @@ var Business = class {
|
|
|
2473
2664
|
...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
|
|
2474
2665
|
}
|
|
2475
2666
|
} : {}
|
|
2476
|
-
}
|
|
2667
|
+
};
|
|
2668
|
+
this.graph.register("policy", name, file, this.policyDependsOn(file));
|
|
2477
2669
|
return { kind: "policy", key: name };
|
|
2478
2670
|
}
|
|
2479
2671
|
plan(key, options) {
|
|
2480
|
-
this.assertNewKey(
|
|
2672
|
+
this.assertNewKey("plan", key, "plan");
|
|
2481
2673
|
const capabilityKeys = (options.capabilities ?? []).map(keyOf);
|
|
2482
2674
|
const capabilityLimits = {
|
|
2483
2675
|
...options.capabilityLimits ?? {}
|
|
@@ -2526,20 +2718,37 @@ var Business = class {
|
|
|
2526
2718
|
if (mergedCaps.length) {
|
|
2527
2719
|
spec.capabilities = mergedCaps;
|
|
2528
2720
|
}
|
|
2529
|
-
this.
|
|
2721
|
+
this.graph.register("plan", key, spec, this.planDependsOn(spec));
|
|
2530
2722
|
return { kind: "plan", key };
|
|
2531
2723
|
}
|
|
2724
|
+
use(...modules) {
|
|
2725
|
+
for (const module of modules) {
|
|
2726
|
+
module(this);
|
|
2727
|
+
}
|
|
2728
|
+
return this;
|
|
2729
|
+
}
|
|
2730
|
+
/** Inspect the SDK-local declaration graph used to emit the Manifest IR. */
|
|
2731
|
+
resourceGraph() {
|
|
2732
|
+
return this.graph.snapshot();
|
|
2733
|
+
}
|
|
2532
2734
|
/** Assemble + validate the Manifest IR. Throws ManifestValidationError
|
|
2533
2735
|
* with structured issues when the declared state is invalid. */
|
|
2534
2736
|
toIR() {
|
|
2737
|
+
this.assertGraphDependenciesSatisfied();
|
|
2535
2738
|
const candidate = {
|
|
2536
2739
|
irVersion: 1,
|
|
2537
2740
|
sdkVersion: SDK_VERSION,
|
|
2538
2741
|
product: this.buildProductSpec(),
|
|
2539
|
-
routes:
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2742
|
+
routes: this.graph.sortedValues(
|
|
2743
|
+
"feature",
|
|
2744
|
+
(file) => file.feature
|
|
2745
|
+
),
|
|
2746
|
+
policies: this.graph.sortedValues(
|
|
2747
|
+
"policy",
|
|
2748
|
+
(file) => file.name
|
|
2749
|
+
),
|
|
2750
|
+
capabilities: this.graph.sortedValues(
|
|
2751
|
+
"capability",
|
|
2543
2752
|
(file) => file.capability
|
|
2544
2753
|
),
|
|
2545
2754
|
runtime: { rollout: null, flags: null, migrations: null }
|
|
@@ -2567,19 +2776,34 @@ var Business = class {
|
|
|
2567
2776
|
...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
|
|
2568
2777
|
},
|
|
2569
2778
|
metering: {
|
|
2570
|
-
meters:
|
|
2779
|
+
meters: this.graph.sortedValues(
|
|
2780
|
+
"meter",
|
|
2781
|
+
(meter) => meter.key
|
|
2782
|
+
),
|
|
2571
2783
|
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2572
2784
|
},
|
|
2573
2785
|
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2574
|
-
...
|
|
2575
|
-
...
|
|
2576
|
-
...this.
|
|
2577
|
-
|
|
2578
|
-
|
|
2786
|
+
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
2787
|
+
...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
|
|
2788
|
+
...this.graph.has("frontend", "manifest") ? {
|
|
2789
|
+
frontend: this.graph.get(
|
|
2790
|
+
"frontend",
|
|
2791
|
+
"manifest"
|
|
2792
|
+
)?.value
|
|
2793
|
+
} : {},
|
|
2794
|
+
...this.graph.values("lifecycle_migration").length ? {
|
|
2795
|
+
migrations: this.graph.sortedValues(
|
|
2796
|
+
"lifecycle_migration",
|
|
2797
|
+
(migration) => migration.id
|
|
2798
|
+
)
|
|
2799
|
+
} : {},
|
|
2800
|
+
...this.graph.values("counted_resource").length ? {
|
|
2801
|
+
resources: this.graph.sortedValues(
|
|
2802
|
+
"counted_resource",
|
|
2579
2803
|
(resource) => resource.name
|
|
2580
2804
|
)
|
|
2581
2805
|
} : {},
|
|
2582
|
-
plans:
|
|
2806
|
+
plans: this.graph.sortedValues("plan", (plan) => plan.key)
|
|
2583
2807
|
};
|
|
2584
2808
|
return deepMerge(
|
|
2585
2809
|
base,
|
|
@@ -2597,6 +2821,7 @@ var Business = class {
|
|
|
2597
2821
|
}
|
|
2598
2822
|
ensureFrontendManifest() {
|
|
2599
2823
|
this.frontendManifest ??= { version: 1, nav: [], pages: [] };
|
|
2824
|
+
this.syncFrontendGraphNode(this.frontendManifest);
|
|
2600
2825
|
return this.frontendManifest;
|
|
2601
2826
|
}
|
|
2602
2827
|
normalizeMigrationPlanRef(ref) {
|
|
@@ -2611,13 +2836,6 @@ var Business = class {
|
|
|
2611
2836
|
version: ref.version ?? "head"
|
|
2612
2837
|
};
|
|
2613
2838
|
}
|
|
2614
|
-
assertUniqueMigrationId(id) {
|
|
2615
|
-
if (this.migrations.some((migration) => migration.id === id)) {
|
|
2616
|
-
throw new ManifestBuilderError(
|
|
2617
|
-
`duplicate migration "${id}" \u2014 each migration id must be declared once`
|
|
2618
|
-
);
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
2839
|
assertUniqueMigrationIds(migrations) {
|
|
2622
2840
|
const seen = /* @__PURE__ */ new Set();
|
|
2623
2841
|
for (const migration of migrations) {
|
|
@@ -2629,30 +2847,154 @@ var Business = class {
|
|
|
2629
2847
|
seen.add(migration.id);
|
|
2630
2848
|
}
|
|
2631
2849
|
}
|
|
2632
|
-
assertNewKey(
|
|
2633
|
-
if (
|
|
2850
|
+
assertNewKey(kind, key, label) {
|
|
2851
|
+
if (this.graph.has(kind, key)) {
|
|
2634
2852
|
throw new ManifestBuilderError(
|
|
2635
2853
|
`duplicate ${label} "${key}" \u2014 each ${label} key must be declared once`
|
|
2636
2854
|
);
|
|
2637
2855
|
}
|
|
2638
2856
|
}
|
|
2857
|
+
assertGraphDependenciesSatisfied() {
|
|
2858
|
+
const missing = this.graph.missingDependencies();
|
|
2859
|
+
if (missing.length === 0) return;
|
|
2860
|
+
const details = missing.slice(0, 8).map(
|
|
2861
|
+
({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
|
|
2862
|
+
dependency
|
|
2863
|
+
)}`
|
|
2864
|
+
).join("; ");
|
|
2865
|
+
const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
|
|
2866
|
+
throw new ManifestBuilderError(
|
|
2867
|
+
`manifest has unresolved resource reference(s): ${details}${suffix}`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
getFeatureFile(key) {
|
|
2871
|
+
return this.graph.get("feature", key)?.value ?? null;
|
|
2872
|
+
}
|
|
2873
|
+
registerFeatureFile(file) {
|
|
2874
|
+
this.graph.register(
|
|
2875
|
+
"feature",
|
|
2876
|
+
file.feature,
|
|
2877
|
+
file,
|
|
2878
|
+
this.featureDependsOn(file)
|
|
2879
|
+
);
|
|
2880
|
+
for (const action of file.actions ?? []) {
|
|
2881
|
+
this.registerAction(file.feature, action);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
syncFeatureGraphNode(file) {
|
|
2885
|
+
this.graph.upsert(
|
|
2886
|
+
"feature",
|
|
2887
|
+
file.feature,
|
|
2888
|
+
file,
|
|
2889
|
+
this.featureDependsOn(file)
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
registerAction(featureKey, action) {
|
|
2893
|
+
this.assertNewKey("action", action.id, "action");
|
|
2894
|
+
this.graph.register(
|
|
2895
|
+
"action",
|
|
2896
|
+
action.id,
|
|
2897
|
+
action,
|
|
2898
|
+
this.actionDependsOn(featureKey, action)
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
syncFrontendGraphNode(manifest) {
|
|
2902
|
+
this.graph.upsert(
|
|
2903
|
+
"frontend",
|
|
2904
|
+
"manifest",
|
|
2905
|
+
manifest,
|
|
2906
|
+
this.frontendDependsOn(manifest)
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
capabilityDependsOn(file) {
|
|
2910
|
+
return [
|
|
2911
|
+
...this.dependenciesFor("feature", file.includes_features),
|
|
2912
|
+
...this.dependenciesFor("policy", file.includes_policies),
|
|
2913
|
+
...this.dependenciesFor("capability", file.includes_capabilities)
|
|
2914
|
+
];
|
|
2915
|
+
}
|
|
2916
|
+
policyDependsOn(file) {
|
|
2917
|
+
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
2918
|
+
}
|
|
2919
|
+
featureDependsOn(file) {
|
|
2920
|
+
const meterKeys = file.routes.flatMap((route) => route.meters ?? []);
|
|
2921
|
+
return [
|
|
2922
|
+
...this.dependenciesFor("policy", file.policies),
|
|
2923
|
+
...this.dependenciesFor("capability", file.capabilities),
|
|
2924
|
+
...this.dependenciesFor("plan", file.plans),
|
|
2925
|
+
...this.dependenciesFor("meter", meterKeys)
|
|
2926
|
+
];
|
|
2927
|
+
}
|
|
2928
|
+
actionDependsOn(featureKey, action) {
|
|
2929
|
+
return [
|
|
2930
|
+
resourceDependency("feature", featureKey),
|
|
2931
|
+
...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
|
|
2932
|
+
];
|
|
2933
|
+
}
|
|
2934
|
+
planDependsOn(plan) {
|
|
2935
|
+
const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
|
|
2936
|
+
const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
|
|
2937
|
+
const pricedMeterDimensions = (plan.meters ?? []).map(
|
|
2938
|
+
(meter) => meter.dimension
|
|
2939
|
+
);
|
|
2940
|
+
const capacityKeys = Object.keys(plan.capability_limits ?? {});
|
|
2941
|
+
return [
|
|
2942
|
+
...this.dependenciesFor("capability", caps),
|
|
2943
|
+
...this.existingDependenciesFor("meter", [
|
|
2944
|
+
...limitDimensions,
|
|
2945
|
+
...pricedMeterDimensions
|
|
2946
|
+
]),
|
|
2947
|
+
...this.existingDependenciesFor("counted_resource", capacityKeys)
|
|
2948
|
+
];
|
|
2949
|
+
}
|
|
2950
|
+
migrationDependsOn(migration) {
|
|
2951
|
+
return [
|
|
2952
|
+
resourceDependency("plan", migration.from.plan),
|
|
2953
|
+
resourceDependency("plan", migration.to.plan),
|
|
2954
|
+
...this.dependenciesFor(
|
|
2955
|
+
"plan",
|
|
2956
|
+
migration.pins?.map((pin) => pin.pinTo.plan)
|
|
2957
|
+
)
|
|
2958
|
+
];
|
|
2959
|
+
}
|
|
2960
|
+
frontendDependsOn(manifest) {
|
|
2961
|
+
return this.dependenciesFor("capability", [
|
|
2962
|
+
...(manifest.nav ?? []).flatMap(
|
|
2963
|
+
(item) => item.capability ? [item.capability] : []
|
|
2964
|
+
),
|
|
2965
|
+
...(manifest.pages ?? []).flatMap((page) => [
|
|
2966
|
+
...page.capability ? [page.capability] : [],
|
|
2967
|
+
...(page.components ?? []).flatMap(
|
|
2968
|
+
(component) => component.capability ? [component.capability] : []
|
|
2969
|
+
)
|
|
2970
|
+
])
|
|
2971
|
+
]);
|
|
2972
|
+
}
|
|
2973
|
+
dependenciesFor(kind, keys) {
|
|
2974
|
+
return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
|
|
2975
|
+
}
|
|
2976
|
+
existingDependenciesFor(kind, keys) {
|
|
2977
|
+
return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
|
|
2978
|
+
}
|
|
2639
2979
|
};
|
|
2640
2980
|
function isBusiness(value) {
|
|
2641
2981
|
return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
|
|
2642
2982
|
}
|
|
2643
|
-
function business(name, options) {
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
return [...items].sort((a, b) => {
|
|
2648
|
-
const ka = key(a);
|
|
2649
|
-
const kb = key(b);
|
|
2650
|
-
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
2651
|
-
});
|
|
2983
|
+
function business(name, options, configure) {
|
|
2984
|
+
const product2 = new Business(name, options);
|
|
2985
|
+
if (configure) product2.use(configure);
|
|
2986
|
+
return product2;
|
|
2652
2987
|
}
|
|
2653
2988
|
function isPlainObject(value) {
|
|
2654
2989
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2655
2990
|
}
|
|
2991
|
+
function buildCustomerContext(options) {
|
|
2992
|
+
return {
|
|
2993
|
+
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
2994
|
+
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
2995
|
+
...options.portalAuth !== void 0 ? { portal_auth: options.portalAuth } : {}
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2656
2998
|
function deepMerge(base, patch) {
|
|
2657
2999
|
const out = { ...base };
|
|
2658
3000
|
for (const [key, value] of Object.entries(patch)) {
|
|
@@ -2665,6 +3007,12 @@ function deepMerge(base, patch) {
|
|
|
2665
3007
|
}
|
|
2666
3008
|
return out;
|
|
2667
3009
|
}
|
|
3010
|
+
function describeResourceUrn(urn) {
|
|
3011
|
+
const parts = urn.split(":");
|
|
3012
|
+
const kind = parts[3] ?? "resource";
|
|
3013
|
+
const key = parts.slice(4).join(":");
|
|
3014
|
+
return `${kind} "${decodeURIComponent(key)}"`;
|
|
3015
|
+
}
|
|
2668
3016
|
|
|
2669
3017
|
// src/price.ts
|
|
2670
3018
|
function toCents(dollars, label) {
|