@farthershore/product 0.0.0 → 0.1.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 +140 -0
- package/dist/bin.js +404 -63
- package/dist/codegen.js +23 -0
- package/dist/index.js +404 -63
- package/dist/types/business.d.ts +28 -9
- 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.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,6 +1437,8 @@ 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
1443
|
* Track B4 — Declarative frontend surface. Product code can declare
|
|
1280
1444
|
* template-owned nav/pages composed from known portal components. The
|
|
@@ -1990,6 +2154,8 @@ var productSpecV2Schema = z20.object({
|
|
|
1990
2154
|
frontend: frontendManifestSchema.optional(),
|
|
1991
2155
|
migrations: migrationDeclsSchema.optional(),
|
|
1992
2156
|
resources: countedResourcesSchema,
|
|
2157
|
+
policies: productOperatorPoliciesSchema,
|
|
2158
|
+
customer_context: productCustomerContextSchema.optional(),
|
|
1993
2159
|
billing: z20.object({
|
|
1994
2160
|
gracePeriodDays: z20.number().int().nonnegative().default(3),
|
|
1995
2161
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
@@ -2190,7 +2356,7 @@ function canonicalIrJson(ir) {
|
|
|
2190
2356
|
}
|
|
2191
2357
|
|
|
2192
2358
|
// src/version.ts
|
|
2193
|
-
var SDK_VERSION = true ? "0.
|
|
2359
|
+
var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
2194
2360
|
|
|
2195
2361
|
// src/business.ts
|
|
2196
2362
|
var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
|
|
@@ -2239,13 +2405,7 @@ var Business = class {
|
|
|
2239
2405
|
[BUSINESS_BRAND] = true;
|
|
2240
2406
|
name;
|
|
2241
2407
|
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 = [];
|
|
2408
|
+
graph = new ManifestResourceGraph();
|
|
2249
2409
|
frontendManifest;
|
|
2250
2410
|
productPatch = {};
|
|
2251
2411
|
/** Sugar for binding API routes to features. */
|
|
@@ -2265,16 +2425,21 @@ var Business = class {
|
|
|
2265
2425
|
}
|
|
2266
2426
|
this.name = name;
|
|
2267
2427
|
this.options = options;
|
|
2428
|
+
this.graph.register("product", name, {
|
|
2429
|
+
name,
|
|
2430
|
+
options
|
|
2431
|
+
});
|
|
2268
2432
|
this.api = {
|
|
2269
2433
|
route: (match, options2) => {
|
|
2270
2434
|
const featureKey = keyOf(options2.feature);
|
|
2271
|
-
const file = this.
|
|
2435
|
+
const file = this.getFeatureFile(featureKey);
|
|
2272
2436
|
if (!file) {
|
|
2273
2437
|
throw new ManifestBuilderError(
|
|
2274
2438
|
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call business.feature("${featureKey}", \u2026) first`
|
|
2275
2439
|
);
|
|
2276
2440
|
}
|
|
2277
2441
|
file.routes.push(this.buildRoute(match, options2));
|
|
2442
|
+
this.syncFeatureGraphNode(file);
|
|
2278
2443
|
return this;
|
|
2279
2444
|
}
|
|
2280
2445
|
};
|
|
@@ -2286,6 +2451,7 @@ var Business = class {
|
|
|
2286
2451
|
path: item.path,
|
|
2287
2452
|
...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
|
|
2288
2453
|
}));
|
|
2454
|
+
this.syncFrontendGraphNode(manifest);
|
|
2289
2455
|
return this;
|
|
2290
2456
|
},
|
|
2291
2457
|
page: (path, options2) => {
|
|
@@ -2310,17 +2476,19 @@ var Business = class {
|
|
|
2310
2476
|
}))
|
|
2311
2477
|
} : {}
|
|
2312
2478
|
});
|
|
2479
|
+
this.syncFrontendGraphNode(manifest);
|
|
2313
2480
|
return this;
|
|
2314
2481
|
},
|
|
2315
2482
|
manifest: (manifest) => {
|
|
2316
2483
|
this.frontendManifest = manifest;
|
|
2484
|
+
this.syncFrontendGraphNode(manifest);
|
|
2317
2485
|
return this;
|
|
2318
2486
|
}
|
|
2319
2487
|
};
|
|
2320
2488
|
this.lifecycle = {
|
|
2321
2489
|
migration: (id, options2) => {
|
|
2322
|
-
this.
|
|
2323
|
-
|
|
2490
|
+
this.assertNewKey("lifecycle_migration", id, "migration");
|
|
2491
|
+
const migration = {
|
|
2324
2492
|
id,
|
|
2325
2493
|
from: this.normalizeMigrationPlanRef(options2.from),
|
|
2326
2494
|
to: this.normalizeMigrationTargetRef(options2.to),
|
|
@@ -2337,12 +2505,26 @@ var Business = class {
|
|
|
2337
2505
|
...pin.notes !== void 0 ? { notes: pin.notes } : {}
|
|
2338
2506
|
}))
|
|
2339
2507
|
} : {}
|
|
2340
|
-
}
|
|
2508
|
+
};
|
|
2509
|
+
this.graph.register(
|
|
2510
|
+
"lifecycle_migration",
|
|
2511
|
+
id,
|
|
2512
|
+
migration,
|
|
2513
|
+
this.migrationDependsOn(migration)
|
|
2514
|
+
);
|
|
2341
2515
|
return this;
|
|
2342
2516
|
},
|
|
2343
2517
|
migrations: (migrations) => {
|
|
2344
2518
|
this.assertUniqueMigrationIds(migrations);
|
|
2345
|
-
this.
|
|
2519
|
+
this.graph.clearKind("lifecycle_migration");
|
|
2520
|
+
for (const migration of migrations) {
|
|
2521
|
+
this.graph.register(
|
|
2522
|
+
"lifecycle_migration",
|
|
2523
|
+
migration.id,
|
|
2524
|
+
migration,
|
|
2525
|
+
this.migrationDependsOn(migration)
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2346
2528
|
return this;
|
|
2347
2529
|
}
|
|
2348
2530
|
};
|
|
@@ -2352,43 +2534,44 @@ var Business = class {
|
|
|
2352
2534
|
return this;
|
|
2353
2535
|
},
|
|
2354
2536
|
plan: (spec) => {
|
|
2355
|
-
this.assertNewKey(
|
|
2356
|
-
this.
|
|
2537
|
+
this.assertNewKey("plan", spec.key, "plan");
|
|
2538
|
+
this.graph.register("plan", spec.key, spec, this.planDependsOn(spec));
|
|
2357
2539
|
return this;
|
|
2358
2540
|
},
|
|
2359
2541
|
routesFile: (file) => {
|
|
2360
|
-
this.assertNewKey(
|
|
2361
|
-
this.
|
|
2542
|
+
this.assertNewKey("feature", file.feature, "feature");
|
|
2543
|
+
this.registerFeatureFile(file);
|
|
2362
2544
|
return this;
|
|
2363
2545
|
},
|
|
2364
2546
|
policyFile: (file) => {
|
|
2365
|
-
this.assertNewKey(
|
|
2366
|
-
this.
|
|
2547
|
+
this.assertNewKey("policy", file.name, "policy");
|
|
2548
|
+
this.graph.register("policy", file.name, file);
|
|
2367
2549
|
return this;
|
|
2368
2550
|
},
|
|
2369
2551
|
capabilityFile: (file) => {
|
|
2370
|
-
this.assertNewKey(
|
|
2371
|
-
this.
|
|
2552
|
+
this.assertNewKey("capability", file.capability, "capability");
|
|
2553
|
+
this.graph.register("capability", file.capability, file);
|
|
2372
2554
|
return this;
|
|
2373
2555
|
},
|
|
2374
2556
|
frontend: (manifest) => {
|
|
2375
2557
|
this.frontendManifest = manifest;
|
|
2558
|
+
this.syncFrontendGraphNode(manifest);
|
|
2376
2559
|
return this;
|
|
2377
2560
|
}
|
|
2378
2561
|
};
|
|
2379
2562
|
}
|
|
2380
2563
|
meter(key, options) {
|
|
2381
|
-
this.assertNewKey(
|
|
2382
|
-
this.
|
|
2564
|
+
this.assertNewKey("meter", key, "meter");
|
|
2565
|
+
this.graph.register("meter", key, { key, ...options });
|
|
2383
2566
|
return { kind: "meter", key };
|
|
2384
2567
|
}
|
|
2385
2568
|
resource(name, options = {}) {
|
|
2386
|
-
this.assertNewKey(
|
|
2387
|
-
this.
|
|
2569
|
+
this.assertNewKey("counted_resource", name, "resource");
|
|
2570
|
+
this.graph.register("counted_resource", name, { name, ...options });
|
|
2388
2571
|
return { kind: "resource", key: name };
|
|
2389
2572
|
}
|
|
2390
2573
|
capability(key, options = {}) {
|
|
2391
|
-
this.assertNewKey(
|
|
2574
|
+
this.assertNewKey("capability", key, "capability");
|
|
2392
2575
|
const file = {
|
|
2393
2576
|
capability: key,
|
|
2394
2577
|
...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
|
|
@@ -2397,7 +2580,12 @@ var Business = class {
|
|
|
2397
2580
|
...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
|
|
2398
2581
|
...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
|
|
2399
2582
|
};
|
|
2400
|
-
this.
|
|
2583
|
+
this.graph.register(
|
|
2584
|
+
"capability",
|
|
2585
|
+
key,
|
|
2586
|
+
file,
|
|
2587
|
+
this.capabilityDependsOn(file)
|
|
2588
|
+
);
|
|
2401
2589
|
return {
|
|
2402
2590
|
kind: "capability",
|
|
2403
2591
|
key,
|
|
@@ -2409,7 +2597,7 @@ var Business = class {
|
|
|
2409
2597
|
};
|
|
2410
2598
|
}
|
|
2411
2599
|
feature(key, options = {}) {
|
|
2412
|
-
this.assertNewKey(
|
|
2600
|
+
this.assertNewKey("feature", key, "feature");
|
|
2413
2601
|
const file = {
|
|
2414
2602
|
feature: key,
|
|
2415
2603
|
routes: [],
|
|
@@ -2436,30 +2624,34 @@ var Business = class {
|
|
|
2436
2624
|
for (const route of options.routes ?? []) {
|
|
2437
2625
|
file.routes.push(this.buildRoute(route.match, route));
|
|
2438
2626
|
}
|
|
2439
|
-
this.
|
|
2627
|
+
this.registerFeatureFile(file);
|
|
2440
2628
|
const ref = {
|
|
2441
2629
|
kind: "feature",
|
|
2442
2630
|
key,
|
|
2443
2631
|
action: (id, actionOptions) => {
|
|
2444
2632
|
file.actions ??= [];
|
|
2445
|
-
if (file.actions.some((
|
|
2633
|
+
if (file.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
|
|
2446
2634
|
throw new ManifestBuilderError(
|
|
2447
2635
|
`duplicate action "${id}" \u2014 each action id must be declared once`
|
|
2448
2636
|
);
|
|
2449
2637
|
}
|
|
2450
|
-
|
|
2638
|
+
const action = { id, ...actionOptions };
|
|
2639
|
+
file.actions.push(action);
|
|
2640
|
+
this.registerAction(key, action);
|
|
2641
|
+
this.syncFeatureGraphNode(file);
|
|
2451
2642
|
return { kind: "action", key: id };
|
|
2452
2643
|
},
|
|
2453
2644
|
route: (match, routeOptions) => {
|
|
2454
2645
|
file.routes.push(this.buildRoute(match, routeOptions ?? {}));
|
|
2646
|
+
this.syncFeatureGraphNode(file);
|
|
2455
2647
|
return ref;
|
|
2456
2648
|
}
|
|
2457
2649
|
};
|
|
2458
2650
|
return ref;
|
|
2459
2651
|
}
|
|
2460
2652
|
policy(name, options) {
|
|
2461
|
-
this.assertNewKey(
|
|
2462
|
-
|
|
2653
|
+
this.assertNewKey("policy", name, "policy");
|
|
2654
|
+
const file = {
|
|
2463
2655
|
name,
|
|
2464
2656
|
type: options.type,
|
|
2465
2657
|
config: options.config,
|
|
@@ -2473,11 +2665,12 @@ var Business = class {
|
|
|
2473
2665
|
...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
|
|
2474
2666
|
}
|
|
2475
2667
|
} : {}
|
|
2476
|
-
}
|
|
2668
|
+
};
|
|
2669
|
+
this.graph.register("policy", name, file, this.policyDependsOn(file));
|
|
2477
2670
|
return { kind: "policy", key: name };
|
|
2478
2671
|
}
|
|
2479
2672
|
plan(key, options) {
|
|
2480
|
-
this.assertNewKey(
|
|
2673
|
+
this.assertNewKey("plan", key, "plan");
|
|
2481
2674
|
const capabilityKeys = (options.capabilities ?? []).map(keyOf);
|
|
2482
2675
|
const capabilityLimits = {
|
|
2483
2676
|
...options.capabilityLimits ?? {}
|
|
@@ -2526,20 +2719,31 @@ var Business = class {
|
|
|
2526
2719
|
if (mergedCaps.length) {
|
|
2527
2720
|
spec.capabilities = mergedCaps;
|
|
2528
2721
|
}
|
|
2529
|
-
this.
|
|
2722
|
+
this.graph.register("plan", key, spec, this.planDependsOn(spec));
|
|
2530
2723
|
return { kind: "plan", key };
|
|
2531
2724
|
}
|
|
2725
|
+
/** Inspect the SDK-local declaration graph used to emit the Manifest IR. */
|
|
2726
|
+
resourceGraph() {
|
|
2727
|
+
return this.graph.snapshot();
|
|
2728
|
+
}
|
|
2532
2729
|
/** Assemble + validate the Manifest IR. Throws ManifestValidationError
|
|
2533
2730
|
* with structured issues when the declared state is invalid. */
|
|
2534
2731
|
toIR() {
|
|
2732
|
+
this.assertGraphDependenciesSatisfied();
|
|
2535
2733
|
const candidate = {
|
|
2536
2734
|
irVersion: 1,
|
|
2537
2735
|
sdkVersion: SDK_VERSION,
|
|
2538
2736
|
product: this.buildProductSpec(),
|
|
2539
|
-
routes:
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2737
|
+
routes: this.graph.sortedValues(
|
|
2738
|
+
"feature",
|
|
2739
|
+
(file) => file.feature
|
|
2740
|
+
),
|
|
2741
|
+
policies: this.graph.sortedValues(
|
|
2742
|
+
"policy",
|
|
2743
|
+
(file) => file.name
|
|
2744
|
+
),
|
|
2745
|
+
capabilities: this.graph.sortedValues(
|
|
2746
|
+
"capability",
|
|
2543
2747
|
(file) => file.capability
|
|
2544
2748
|
),
|
|
2545
2749
|
runtime: { rollout: null, flags: null, migrations: null }
|
|
@@ -2567,19 +2771,34 @@ var Business = class {
|
|
|
2567
2771
|
...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
|
|
2568
2772
|
},
|
|
2569
2773
|
metering: {
|
|
2570
|
-
meters:
|
|
2774
|
+
meters: this.graph.sortedValues(
|
|
2775
|
+
"meter",
|
|
2776
|
+
(meter) => meter.key
|
|
2777
|
+
),
|
|
2571
2778
|
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2572
2779
|
},
|
|
2573
2780
|
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2574
|
-
...
|
|
2575
|
-
...
|
|
2576
|
-
...this.
|
|
2577
|
-
|
|
2578
|
-
|
|
2781
|
+
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
2782
|
+
...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
|
|
2783
|
+
...this.graph.has("frontend", "manifest") ? {
|
|
2784
|
+
frontend: this.graph.get(
|
|
2785
|
+
"frontend",
|
|
2786
|
+
"manifest"
|
|
2787
|
+
)?.value
|
|
2788
|
+
} : {},
|
|
2789
|
+
...this.graph.values("lifecycle_migration").length ? {
|
|
2790
|
+
migrations: this.graph.sortedValues(
|
|
2791
|
+
"lifecycle_migration",
|
|
2792
|
+
(migration) => migration.id
|
|
2793
|
+
)
|
|
2794
|
+
} : {},
|
|
2795
|
+
...this.graph.values("counted_resource").length ? {
|
|
2796
|
+
resources: this.graph.sortedValues(
|
|
2797
|
+
"counted_resource",
|
|
2579
2798
|
(resource) => resource.name
|
|
2580
2799
|
)
|
|
2581
2800
|
} : {},
|
|
2582
|
-
plans:
|
|
2801
|
+
plans: this.graph.sortedValues("plan", (plan) => plan.key)
|
|
2583
2802
|
};
|
|
2584
2803
|
return deepMerge(
|
|
2585
2804
|
base,
|
|
@@ -2597,6 +2816,7 @@ var Business = class {
|
|
|
2597
2816
|
}
|
|
2598
2817
|
ensureFrontendManifest() {
|
|
2599
2818
|
this.frontendManifest ??= { version: 1, nav: [], pages: [] };
|
|
2819
|
+
this.syncFrontendGraphNode(this.frontendManifest);
|
|
2600
2820
|
return this.frontendManifest;
|
|
2601
2821
|
}
|
|
2602
2822
|
normalizeMigrationPlanRef(ref) {
|
|
@@ -2611,13 +2831,6 @@ var Business = class {
|
|
|
2611
2831
|
version: ref.version ?? "head"
|
|
2612
2832
|
};
|
|
2613
2833
|
}
|
|
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
2834
|
assertUniqueMigrationIds(migrations) {
|
|
2622
2835
|
const seen = /* @__PURE__ */ new Set();
|
|
2623
2836
|
for (const migration of migrations) {
|
|
@@ -2629,13 +2842,135 @@ var Business = class {
|
|
|
2629
2842
|
seen.add(migration.id);
|
|
2630
2843
|
}
|
|
2631
2844
|
}
|
|
2632
|
-
assertNewKey(
|
|
2633
|
-
if (
|
|
2845
|
+
assertNewKey(kind, key, label) {
|
|
2846
|
+
if (this.graph.has(kind, key)) {
|
|
2634
2847
|
throw new ManifestBuilderError(
|
|
2635
2848
|
`duplicate ${label} "${key}" \u2014 each ${label} key must be declared once`
|
|
2636
2849
|
);
|
|
2637
2850
|
}
|
|
2638
2851
|
}
|
|
2852
|
+
assertGraphDependenciesSatisfied() {
|
|
2853
|
+
const missing = this.graph.missingDependencies();
|
|
2854
|
+
if (missing.length === 0) return;
|
|
2855
|
+
const details = missing.slice(0, 8).map(
|
|
2856
|
+
({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
|
|
2857
|
+
dependency
|
|
2858
|
+
)}`
|
|
2859
|
+
).join("; ");
|
|
2860
|
+
const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
|
|
2861
|
+
throw new ManifestBuilderError(
|
|
2862
|
+
`manifest has unresolved resource reference(s): ${details}${suffix}`
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
getFeatureFile(key) {
|
|
2866
|
+
return this.graph.get("feature", key)?.value ?? null;
|
|
2867
|
+
}
|
|
2868
|
+
registerFeatureFile(file) {
|
|
2869
|
+
this.graph.register(
|
|
2870
|
+
"feature",
|
|
2871
|
+
file.feature,
|
|
2872
|
+
file,
|
|
2873
|
+
this.featureDependsOn(file)
|
|
2874
|
+
);
|
|
2875
|
+
for (const action of file.actions ?? []) {
|
|
2876
|
+
this.registerAction(file.feature, action);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
syncFeatureGraphNode(file) {
|
|
2880
|
+
this.graph.upsert(
|
|
2881
|
+
"feature",
|
|
2882
|
+
file.feature,
|
|
2883
|
+
file,
|
|
2884
|
+
this.featureDependsOn(file)
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
registerAction(featureKey, action) {
|
|
2888
|
+
this.assertNewKey("action", action.id, "action");
|
|
2889
|
+
this.graph.register(
|
|
2890
|
+
"action",
|
|
2891
|
+
action.id,
|
|
2892
|
+
action,
|
|
2893
|
+
this.actionDependsOn(featureKey, action)
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
syncFrontendGraphNode(manifest) {
|
|
2897
|
+
this.graph.upsert(
|
|
2898
|
+
"frontend",
|
|
2899
|
+
"manifest",
|
|
2900
|
+
manifest,
|
|
2901
|
+
this.frontendDependsOn(manifest)
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
capabilityDependsOn(file) {
|
|
2905
|
+
return [
|
|
2906
|
+
...this.dependenciesFor("feature", file.includes_features),
|
|
2907
|
+
...this.dependenciesFor("policy", file.includes_policies),
|
|
2908
|
+
...this.dependenciesFor("capability", file.includes_capabilities)
|
|
2909
|
+
];
|
|
2910
|
+
}
|
|
2911
|
+
policyDependsOn(file) {
|
|
2912
|
+
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
2913
|
+
}
|
|
2914
|
+
featureDependsOn(file) {
|
|
2915
|
+
const meterKeys = file.routes.flatMap((route) => route.meters ?? []);
|
|
2916
|
+
return [
|
|
2917
|
+
...this.dependenciesFor("policy", file.policies),
|
|
2918
|
+
...this.dependenciesFor("capability", file.capabilities),
|
|
2919
|
+
...this.dependenciesFor("plan", file.plans),
|
|
2920
|
+
...this.dependenciesFor("meter", meterKeys)
|
|
2921
|
+
];
|
|
2922
|
+
}
|
|
2923
|
+
actionDependsOn(featureKey, action) {
|
|
2924
|
+
return [
|
|
2925
|
+
resourceDependency("feature", featureKey),
|
|
2926
|
+
...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
|
|
2927
|
+
];
|
|
2928
|
+
}
|
|
2929
|
+
planDependsOn(plan) {
|
|
2930
|
+
const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
|
|
2931
|
+
const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
|
|
2932
|
+
const pricedMeterDimensions = (plan.meters ?? []).map(
|
|
2933
|
+
(meter) => meter.dimension
|
|
2934
|
+
);
|
|
2935
|
+
const capacityKeys = Object.keys(plan.capability_limits ?? {});
|
|
2936
|
+
return [
|
|
2937
|
+
...this.dependenciesFor("capability", caps),
|
|
2938
|
+
...this.existingDependenciesFor("meter", [
|
|
2939
|
+
...limitDimensions,
|
|
2940
|
+
...pricedMeterDimensions
|
|
2941
|
+
]),
|
|
2942
|
+
...this.existingDependenciesFor("counted_resource", capacityKeys)
|
|
2943
|
+
];
|
|
2944
|
+
}
|
|
2945
|
+
migrationDependsOn(migration) {
|
|
2946
|
+
return [
|
|
2947
|
+
resourceDependency("plan", migration.from.plan),
|
|
2948
|
+
resourceDependency("plan", migration.to.plan),
|
|
2949
|
+
...this.dependenciesFor(
|
|
2950
|
+
"plan",
|
|
2951
|
+
migration.pins?.map((pin) => pin.pinTo.plan)
|
|
2952
|
+
)
|
|
2953
|
+
];
|
|
2954
|
+
}
|
|
2955
|
+
frontendDependsOn(manifest) {
|
|
2956
|
+
return this.dependenciesFor("capability", [
|
|
2957
|
+
...(manifest.nav ?? []).flatMap(
|
|
2958
|
+
(item) => item.capability ? [item.capability] : []
|
|
2959
|
+
),
|
|
2960
|
+
...(manifest.pages ?? []).flatMap((page) => [
|
|
2961
|
+
...page.capability ? [page.capability] : [],
|
|
2962
|
+
...(page.components ?? []).flatMap(
|
|
2963
|
+
(component) => component.capability ? [component.capability] : []
|
|
2964
|
+
)
|
|
2965
|
+
])
|
|
2966
|
+
]);
|
|
2967
|
+
}
|
|
2968
|
+
dependenciesFor(kind, keys) {
|
|
2969
|
+
return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
|
|
2970
|
+
}
|
|
2971
|
+
existingDependenciesFor(kind, keys) {
|
|
2972
|
+
return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
|
|
2973
|
+
}
|
|
2639
2974
|
};
|
|
2640
2975
|
function isBusiness(value) {
|
|
2641
2976
|
return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
|
|
@@ -2643,16 +2978,16 @@ function isBusiness(value) {
|
|
|
2643
2978
|
function business(name, options) {
|
|
2644
2979
|
return new Business(name, options);
|
|
2645
2980
|
}
|
|
2646
|
-
function sortBy(items, key) {
|
|
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
|
-
});
|
|
2652
|
-
}
|
|
2653
2981
|
function isPlainObject(value) {
|
|
2654
2982
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2655
2983
|
}
|
|
2984
|
+
function buildCustomerContext(options) {
|
|
2985
|
+
return {
|
|
2986
|
+
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
2987
|
+
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
2988
|
+
...options.portalAuth !== void 0 ? { portal_auth: options.portalAuth } : {}
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2656
2991
|
function deepMerge(base, patch) {
|
|
2657
2992
|
const out = { ...base };
|
|
2658
2993
|
for (const [key, value] of Object.entries(patch)) {
|
|
@@ -2665,6 +3000,12 @@ function deepMerge(base, patch) {
|
|
|
2665
3000
|
}
|
|
2666
3001
|
return out;
|
|
2667
3002
|
}
|
|
3003
|
+
function describeResourceUrn(urn) {
|
|
3004
|
+
const parts = urn.split(":");
|
|
3005
|
+
const kind = parts[3] ?? "resource";
|
|
3006
|
+
const key = parts.slice(4).join(":");
|
|
3007
|
+
return `${kind} "${decodeURIComponent(key)}"`;
|
|
3008
|
+
}
|
|
2668
3009
|
|
|
2669
3010
|
// src/price.ts
|
|
2670
3011
|
function toCents(dollars, label) {
|