@farthershore/product 0.3.1 → 0.5.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 +42 -31
- package/dist/bin.js +252 -35
- package/dist/codegen.js +43 -16
- package/dist/index.js +253 -41
- package/dist/types/errors.d.ts +3 -3
- package/dist/types/index.d.ts +4 -6
- package/dist/types/ir-types.d.ts +46 -1
- package/dist/types/{business.d.ts → product.d.ts} +74 -33
- package/dist/types/resource-graph.d.ts +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Product-as-Code SDK for Farther Shore. Builder repos use this package from
|
|
4
4
|
`product/product.config.ts` to declare product contracts in TypeScript. Builders
|
|
5
|
-
author and export a `
|
|
6
|
-
backend-owned IR, validates it, and applies it through Core.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
author and export a `Product`; Farther Shore compiles that program to
|
|
6
|
+
backend-owned IR, validates it, and applies it through Core. The Product SDK
|
|
7
|
+
describes the sellable software product: its business logic origin,
|
|
8
|
+
surfaces, plans, capabilities, meters, and lifecycle. Every product is created
|
|
9
|
+
with a GitHub repo that contains the editable `frontend/` starter and the
|
|
10
|
+
Product SDK entrypoint; connecting GitHub is a product-creation precondition.
|
|
10
11
|
|
|
11
12
|
## Install
|
|
12
13
|
|
|
@@ -47,15 +48,17 @@ import { configureMeters } from "./meters.js";
|
|
|
47
48
|
import { configureCronRoutes } from "./routes/cron.js";
|
|
48
49
|
import { configurePlans } from "./plans/index.js";
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
|
|
51
|
+
const product = fs.product("croncloud", {
|
|
52
|
+
origin: "https://app.example.com",
|
|
52
53
|
displayName: "CronCloud",
|
|
53
54
|
description: "Managed cron jobs",
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
product.surface("frontend");
|
|
58
|
+
product.surface("api");
|
|
59
|
+
product.use(configureMeters, configureCronRoutes, configurePlans);
|
|
57
60
|
|
|
58
|
-
export default
|
|
61
|
+
export default product;
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
Modules are plain synchronous functions:
|
|
@@ -100,7 +103,7 @@ plain request counting.
|
|
|
100
103
|
|
|
101
104
|
```ts
|
|
102
105
|
const product = fs.product("croncloud", {
|
|
103
|
-
|
|
106
|
+
origin: "https://app.croncloud.com",
|
|
104
107
|
});
|
|
105
108
|
|
|
106
109
|
product.requests();
|
|
@@ -145,18 +148,18 @@ Generated product repos use GitHub as the required automation and frontend
|
|
|
145
148
|
workspace:
|
|
146
149
|
|
|
147
150
|
1. Loads `product/product.config.ts`.
|
|
148
|
-
2. Requires the default export to be the `
|
|
149
|
-
`fs.
|
|
150
|
-
3. Executes imported modules and compiles the
|
|
151
|
+
2. Requires the default export to be the `Product` returned by
|
|
152
|
+
`fs.product(...)`.
|
|
153
|
+
3. Executes imported modules and compiles the product into deterministic
|
|
151
154
|
Manifest IR.
|
|
152
155
|
4. Validates the result against the deployed platform contract.
|
|
153
156
|
5. Publishes the accepted release through Core so edge artifacts propagate.
|
|
154
157
|
|
|
155
|
-
The same IR can
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
The same IR can be built locally with `farthershore build`; accepted lifecycle
|
|
159
|
+
state is validated and applied by the GitHub bot after `product/**` changes are
|
|
160
|
+
committed and pushed. The repo remains the required frontend customization
|
|
161
|
+
workspace because `frontend/` is where the starter UI and all custom React code
|
|
162
|
+
live.
|
|
160
163
|
|
|
161
164
|
The bundled `farthershore-manifest-build` binary is shared by the bot,
|
|
162
165
|
build-runner, and CLI. It emits the deterministic Manifest IR envelope that Core
|
|
@@ -164,24 +167,32 @@ accepts; Core, not the user repo, remains the lifecycle authority.
|
|
|
164
167
|
|
|
165
168
|
## Public API
|
|
166
169
|
|
|
167
|
-
- `fs.
|
|
168
|
-
|
|
169
|
-
-
|
|
170
|
+
- `fs.product(name, options)` — create the product builder. `options.origin`
|
|
171
|
+
is required and is the business logic origin Farther Shore calls for
|
|
172
|
+
customer-facing actions.
|
|
173
|
+
- `product.use(...modules)` — compose Product SDK modules from any files under
|
|
170
174
|
`product/`.
|
|
171
|
-
- `
|
|
172
|
-
|
|
175
|
+
- `product.surface(...)` — declare the product surfaces customers interact with
|
|
176
|
+
(`frontend`, `api`, `docs`, `widget`, `webhook`, `worker`, or `agent`).
|
|
177
|
+
- `product.entitlement(...)` — group capabilities, feature gates, limits, and
|
|
178
|
+
meters into reusable access metadata.
|
|
179
|
+
- `product.offering.plan(...)` — declare a subscription plan through the
|
|
180
|
+
generalized offering namespace. `product.plan(...)` remains the direct plan
|
|
181
|
+
helper.
|
|
182
|
+
- `product.meter(...)` — declare billable or enforceable dimensions.
|
|
183
|
+
- `product.requests()` — declare and inherit the platform-managed successful
|
|
173
184
|
request meter.
|
|
174
|
-
- `
|
|
175
|
-
- `
|
|
185
|
+
- `product.defaultMeters(...)` — apply reusable fixed costs to metered routes.
|
|
186
|
+
- `product.resource(...)` — declare counted resources for resource-count
|
|
176
187
|
constraints.
|
|
177
|
-
- `
|
|
178
|
-
- `
|
|
188
|
+
- `product.capability(...)` — declare capability bundles and plan grants.
|
|
189
|
+
- `product.feature(...)` / `product.api.route(...)` — declare gateway routes,
|
|
179
190
|
static costs, dynamic reports, estimates, and action metadata.
|
|
180
|
-
- `
|
|
181
|
-
- `
|
|
191
|
+
- `product.policy(...)` — declare policy files in code.
|
|
192
|
+
- `product.plan(...)` — declare plan pricing, limits, grants, and lifecycle
|
|
182
193
|
behavior.
|
|
183
|
-
- `
|
|
184
|
-
- `
|
|
194
|
+
- `product.lifecycle.*` — declare migrations.
|
|
195
|
+
- `product.raw.*` — escape hatches for platform-schema JSON when the typed SDK
|
|
185
196
|
does not yet have sugar.
|
|
186
197
|
|
|
187
198
|
## Determinism
|
package/dist/bin.js
CHANGED
|
@@ -1393,7 +1393,7 @@ var customerPortalAuthStrategySchema = z13.enum([
|
|
|
1393
1393
|
var productCustomerContextSchema = z13.object({
|
|
1394
1394
|
/**
|
|
1395
1395
|
* Edge credential identity policy. This is intentionally Product-scoped:
|
|
1396
|
-
* B7 keeps Product as the
|
|
1396
|
+
* B7 keeps Product as the product boundary and avoids customer-side
|
|
1397
1397
|
* Workspace/subject models until per-subject entitlements need them.
|
|
1398
1398
|
*/
|
|
1399
1399
|
identity_requirement: customerIdentityRequirementSchema.optional(),
|
|
@@ -1414,6 +1414,70 @@ var productCustomerContextSchema = z13.object({
|
|
|
1414
1414
|
strategy: customerPortalAuthStrategySchema
|
|
1415
1415
|
}).optional()
|
|
1416
1416
|
});
|
|
1417
|
+
var productSurfaceTypeSchema = z13.enum([
|
|
1418
|
+
"frontend",
|
|
1419
|
+
"api",
|
|
1420
|
+
"docs",
|
|
1421
|
+
"widget",
|
|
1422
|
+
"dashboard",
|
|
1423
|
+
"webhook",
|
|
1424
|
+
"worker",
|
|
1425
|
+
"agent"
|
|
1426
|
+
]);
|
|
1427
|
+
var productSurfaceSchema = z13.object({
|
|
1428
|
+
key: z13.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Surface key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1429
|
+
type: productSurfaceTypeSchema,
|
|
1430
|
+
display: z13.string().min(1).max(100).optional(),
|
|
1431
|
+
description: z13.string().max(500).optional()
|
|
1432
|
+
});
|
|
1433
|
+
var productEntitlementSchema = z13.object({
|
|
1434
|
+
key: z13.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Entitlement key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1435
|
+
description: z13.string().max(500).optional(),
|
|
1436
|
+
capabilities: z13.array(z13.string().min(1).max(100)).max(100).optional(),
|
|
1437
|
+
featureGates: z13.record(z13.string().min(1), z13.boolean()).optional(),
|
|
1438
|
+
limits: z13.array(planLimitRuleSchema).max(100).optional(),
|
|
1439
|
+
meters: z13.array(z13.string().min(1).max(64)).max(100).optional()
|
|
1440
|
+
});
|
|
1441
|
+
var productSurfacesSchema = z13.array(productSurfaceSchema).max(20).default([]);
|
|
1442
|
+
var productEntitlementsSchema = z13.array(productEntitlementSchema).max(100).default([]);
|
|
1443
|
+
var productWorkflowKindSchema = z13.enum([
|
|
1444
|
+
"async_job",
|
|
1445
|
+
"agent_task",
|
|
1446
|
+
"scheduled",
|
|
1447
|
+
"lifecycle",
|
|
1448
|
+
"background"
|
|
1449
|
+
]);
|
|
1450
|
+
var productWorkflowTriggerSchema = z13.discriminatedUnion("type", [
|
|
1451
|
+
z13.object({ type: z13.literal("manual") }),
|
|
1452
|
+
z13.object({
|
|
1453
|
+
type: z13.literal("schedule"),
|
|
1454
|
+
cron: z13.string().min(1).max(120)
|
|
1455
|
+
}),
|
|
1456
|
+
z13.object({
|
|
1457
|
+
type: z13.literal("event"),
|
|
1458
|
+
event: z13.string().min(1).max(120)
|
|
1459
|
+
}),
|
|
1460
|
+
z13.object({
|
|
1461
|
+
type: z13.literal("api"),
|
|
1462
|
+
path: z13.string().min(1).max(240).regex(/^\//, "path must start with /")
|
|
1463
|
+
}),
|
|
1464
|
+
z13.object({
|
|
1465
|
+
type: z13.literal("lifecycle"),
|
|
1466
|
+
event: z13.string().min(1).max(120)
|
|
1467
|
+
})
|
|
1468
|
+
]);
|
|
1469
|
+
var productWorkflowSchema = z13.object({
|
|
1470
|
+
key: z13.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Workflow key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1471
|
+
title: z13.string().min(1).max(120).optional(),
|
|
1472
|
+
description: z13.string().max(1e3).optional(),
|
|
1473
|
+
kind: productWorkflowKindSchema,
|
|
1474
|
+
trigger: productWorkflowTriggerSchema,
|
|
1475
|
+
capabilities: z13.array(z13.string().min(1).max(100)).max(100).optional(),
|
|
1476
|
+
meters: z13.array(z13.string().min(1).max(64)).max(100).optional(),
|
|
1477
|
+
estimates: z13.record(z13.string().min(1).max(64), z13.number().finite()).optional(),
|
|
1478
|
+
metadata: z13.record(z13.string().min(1), z13.unknown()).optional()
|
|
1479
|
+
});
|
|
1480
|
+
var productWorkflowsSchema = z13.array(productWorkflowSchema).max(100).default([]);
|
|
1417
1481
|
var productSpecSchema = z13.object({
|
|
1418
1482
|
product: z13.object({
|
|
1419
1483
|
name: z13.string().min(1).max(100),
|
|
@@ -1447,6 +1511,9 @@ var productSpecSchema = z13.object({
|
|
|
1447
1511
|
resources: countedResourcesSchema,
|
|
1448
1512
|
policies: productOperatorPoliciesSchema,
|
|
1449
1513
|
customer_context: productCustomerContextSchema.optional(),
|
|
1514
|
+
surfaces: productSurfacesSchema,
|
|
1515
|
+
entitlements: productEntitlementsSchema,
|
|
1516
|
+
workflows: productWorkflowsSchema,
|
|
1450
1517
|
/**
|
|
1451
1518
|
* Legacy/internal declarative frontend surface. New product repos customize
|
|
1452
1519
|
* the generated `frontend/` project directly; this remains for existing IR
|
|
@@ -2156,6 +2223,9 @@ var productSpecV2Schema = z20.object({
|
|
|
2156
2223
|
resources: countedResourcesSchema,
|
|
2157
2224
|
policies: productOperatorPoliciesSchema,
|
|
2158
2225
|
customer_context: productCustomerContextSchema.optional(),
|
|
2226
|
+
surfaces: productSurfacesSchema,
|
|
2227
|
+
entitlements: productEntitlementsSchema,
|
|
2228
|
+
workflows: productWorkflowsSchema,
|
|
2159
2229
|
billing: z20.object({
|
|
2160
2230
|
gracePeriodDays: z20.number().int().nonnegative().default(3),
|
|
2161
2231
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
@@ -2353,10 +2423,13 @@ function hashIr(ir) {
|
|
|
2353
2423
|
}
|
|
2354
2424
|
|
|
2355
2425
|
// src/version.ts
|
|
2356
|
-
var SDK_VERSION = true ? "0.
|
|
2426
|
+
var SDK_VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
2357
2427
|
|
|
2358
|
-
// src/
|
|
2359
|
-
var
|
|
2428
|
+
// src/product.ts
|
|
2429
|
+
var PRODUCT_BRAND = Symbol.for("farthershore.product.product");
|
|
2430
|
+
var PRODUCT_MANIFEST_COMPILER = Symbol.for(
|
|
2431
|
+
"farthershore.product.manifestCompiler"
|
|
2432
|
+
);
|
|
2360
2433
|
function isCapabilityGrant(value) {
|
|
2361
2434
|
return typeof value === "object" && value !== null && value.kind === "capability_grant";
|
|
2362
2435
|
}
|
|
@@ -2401,8 +2474,8 @@ function parseRouteMatch(match) {
|
|
|
2401
2474
|
}
|
|
2402
2475
|
return { method, path };
|
|
2403
2476
|
}
|
|
2404
|
-
var
|
|
2405
|
-
[
|
|
2477
|
+
var Product = class {
|
|
2478
|
+
[PRODUCT_BRAND] = true;
|
|
2406
2479
|
name;
|
|
2407
2480
|
options;
|
|
2408
2481
|
graph = new ManifestResourceGraph();
|
|
@@ -2413,15 +2486,16 @@ var Business = class {
|
|
|
2413
2486
|
api;
|
|
2414
2487
|
frontend;
|
|
2415
2488
|
lifecycle;
|
|
2416
|
-
|
|
2489
|
+
offering;
|
|
2490
|
+
/** Escape hatches — raw platform-schema JSON, validated by the compiler. */
|
|
2417
2491
|
raw;
|
|
2418
2492
|
constructor(name, options) {
|
|
2419
2493
|
if (!name || typeof name !== "string") {
|
|
2420
|
-
throw new ManifestBuilderError("fs.
|
|
2494
|
+
throw new ManifestBuilderError("fs.product(name, \u2026): name is required");
|
|
2421
2495
|
}
|
|
2422
|
-
if (!options?.
|
|
2496
|
+
if (!options?.origin) {
|
|
2423
2497
|
throw new ManifestBuilderError(
|
|
2424
|
-
`fs.
|
|
2498
|
+
`fs.product("${name}", \u2026): options.origin is required (the business logic origin Farther Shore calls for customer-facing actions)`
|
|
2425
2499
|
);
|
|
2426
2500
|
}
|
|
2427
2501
|
this.name = name;
|
|
@@ -2436,7 +2510,7 @@ var Business = class {
|
|
|
2436
2510
|
const file = this.getFeatureFile(featureKey);
|
|
2437
2511
|
if (!file) {
|
|
2438
2512
|
throw new ManifestBuilderError(
|
|
2439
|
-
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call
|
|
2513
|
+
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call product.feature("${featureKey}", \u2026) first`
|
|
2440
2514
|
);
|
|
2441
2515
|
}
|
|
2442
2516
|
file.routes.push(this.buildRoute(match, options2));
|
|
@@ -2529,6 +2603,9 @@ var Business = class {
|
|
|
2529
2603
|
return this;
|
|
2530
2604
|
}
|
|
2531
2605
|
};
|
|
2606
|
+
this.offering = {
|
|
2607
|
+
plan: (key, options2) => this.plan(key, options2)
|
|
2608
|
+
};
|
|
2532
2609
|
this.raw = {
|
|
2533
2610
|
productPatch: (patch) => {
|
|
2534
2611
|
this.productPatch = deepMerge(this.productPatch, patch);
|
|
@@ -2699,6 +2776,57 @@ var Business = class {
|
|
|
2699
2776
|
this.graph.register("policy", name, file, this.policyDependsOn(file));
|
|
2700
2777
|
return { kind: "policy", key: name };
|
|
2701
2778
|
}
|
|
2779
|
+
surface(type, options = {}) {
|
|
2780
|
+
const key = options.key ?? type;
|
|
2781
|
+
this.assertNewKey("surface", key, "surface");
|
|
2782
|
+
const surface = {
|
|
2783
|
+
key,
|
|
2784
|
+
type,
|
|
2785
|
+
...options.display !== void 0 ? { display: options.display } : {},
|
|
2786
|
+
...options.description !== void 0 ? { description: options.description } : {}
|
|
2787
|
+
};
|
|
2788
|
+
this.graph.register("surface", key, surface);
|
|
2789
|
+
return { kind: "surface", key };
|
|
2790
|
+
}
|
|
2791
|
+
workflow(key, options = {}) {
|
|
2792
|
+
this.assertNewKey("workflow", key, "workflow");
|
|
2793
|
+
const workflow = {
|
|
2794
|
+
key,
|
|
2795
|
+
kind: options.kind ?? "async_job",
|
|
2796
|
+
trigger: options.trigger ?? { type: "manual" },
|
|
2797
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
2798
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2799
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2800
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
|
|
2801
|
+
...options.estimates !== void 0 ? { estimates: options.estimates } : {},
|
|
2802
|
+
...options.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
2803
|
+
};
|
|
2804
|
+
this.graph.register(
|
|
2805
|
+
"workflow",
|
|
2806
|
+
key,
|
|
2807
|
+
workflow,
|
|
2808
|
+
this.workflowDependsOn(workflow)
|
|
2809
|
+
);
|
|
2810
|
+
return { kind: "workflow", key };
|
|
2811
|
+
}
|
|
2812
|
+
entitlement(key, options = {}) {
|
|
2813
|
+
this.assertNewKey("entitlement", key, "entitlement");
|
|
2814
|
+
const entitlement = {
|
|
2815
|
+
key,
|
|
2816
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2817
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2818
|
+
...options.featureGates !== void 0 ? { featureGates: options.featureGates } : {},
|
|
2819
|
+
...options.limits?.length ? { limits: options.limits } : {},
|
|
2820
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {}
|
|
2821
|
+
};
|
|
2822
|
+
this.graph.register(
|
|
2823
|
+
"entitlement",
|
|
2824
|
+
key,
|
|
2825
|
+
entitlement,
|
|
2826
|
+
this.entitlementDependsOn(entitlement)
|
|
2827
|
+
);
|
|
2828
|
+
return { kind: "entitlement", key };
|
|
2829
|
+
}
|
|
2702
2830
|
plan(key, options) {
|
|
2703
2831
|
this.assertNewKey("plan", key, "plan");
|
|
2704
2832
|
const capabilityKeys = (options.capabilities ?? []).map(keyOf);
|
|
@@ -2762,19 +2890,17 @@ var Business = class {
|
|
|
2762
2890
|
resourceGraph() {
|
|
2763
2891
|
return this.graph.snapshot();
|
|
2764
2892
|
}
|
|
2765
|
-
/**
|
|
2766
|
-
*
|
|
2767
|
-
|
|
2893
|
+
/** @internal Internal platform compiler entrypoint. Builder code exports the
|
|
2894
|
+
* Product; the bot/CLI/build-runner decide when to compile and apply it. */
|
|
2895
|
+
[PRODUCT_MANIFEST_COMPILER]() {
|
|
2896
|
+
const routes = this.materializeFeatureFiles();
|
|
2897
|
+
this.assertRouteMeteringValid(routes);
|
|
2768
2898
|
this.assertGraphDependenciesSatisfied();
|
|
2769
|
-
this.assertRouteMeteringValid();
|
|
2770
2899
|
const candidate = {
|
|
2771
2900
|
irVersion: 1,
|
|
2772
2901
|
sdkVersion: SDK_VERSION,
|
|
2773
2902
|
product: this.buildProductSpec(),
|
|
2774
|
-
routes
|
|
2775
|
-
"feature",
|
|
2776
|
-
(file) => file.feature
|
|
2777
|
-
),
|
|
2903
|
+
routes,
|
|
2778
2904
|
policies: this.graph.sortedValues(
|
|
2779
2905
|
"policy",
|
|
2780
2906
|
(file) => file.name
|
|
@@ -2794,10 +2920,10 @@ var Business = class {
|
|
|
2794
2920
|
const base = {
|
|
2795
2921
|
product: {
|
|
2796
2922
|
name: this.name,
|
|
2797
|
-
baseUrl: options.
|
|
2923
|
+
baseUrl: options.origin,
|
|
2798
2924
|
...options.displayName !== void 0 ? { displayName: options.displayName } : {},
|
|
2799
2925
|
...options.description !== void 0 ? { description: options.description } : {},
|
|
2800
|
-
...options.
|
|
2926
|
+
...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
|
|
2801
2927
|
...options.visibility !== void 0 ? { visibility: options.visibility } : {},
|
|
2802
2928
|
...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
|
|
2803
2929
|
...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
|
|
@@ -2814,6 +2940,24 @@ var Business = class {
|
|
|
2814
2940
|
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2815
2941
|
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
2816
2942
|
...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
|
|
2943
|
+
...this.graph.values("surface").length ? {
|
|
2944
|
+
surfaces: this.graph.sortedValues(
|
|
2945
|
+
"surface",
|
|
2946
|
+
(surface) => surface.key
|
|
2947
|
+
)
|
|
2948
|
+
} : {},
|
|
2949
|
+
...this.graph.values("entitlement").length ? {
|
|
2950
|
+
entitlements: this.graph.sortedValues(
|
|
2951
|
+
"entitlement",
|
|
2952
|
+
(entitlement) => entitlement.key
|
|
2953
|
+
)
|
|
2954
|
+
} : {},
|
|
2955
|
+
...this.graph.values("workflow").length ? {
|
|
2956
|
+
workflows: this.graph.sortedValues(
|
|
2957
|
+
"workflow",
|
|
2958
|
+
(workflow) => workflow.key
|
|
2959
|
+
)
|
|
2960
|
+
} : {},
|
|
2817
2961
|
...this.graph.has("frontend", "manifest") ? {
|
|
2818
2962
|
frontend: this.graph.get(
|
|
2819
2963
|
"frontend",
|
|
@@ -2849,6 +2993,9 @@ var Business = class {
|
|
|
2849
2993
|
}
|
|
2850
2994
|
routeValueMeterKeys() {
|
|
2851
2995
|
const keys = /* @__PURE__ */ new Set();
|
|
2996
|
+
for (const cost of this.defaultMeterCosts) {
|
|
2997
|
+
keys.add(cost.meter);
|
|
2998
|
+
}
|
|
2852
2999
|
for (const file of this.graph.values("feature")) {
|
|
2853
3000
|
for (const route of file.routes) {
|
|
2854
3001
|
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
@@ -2859,6 +3006,11 @@ var Business = class {
|
|
|
2859
3006
|
}
|
|
2860
3007
|
}
|
|
2861
3008
|
}
|
|
3009
|
+
for (const workflow of this.graph.values("workflow")) {
|
|
3010
|
+
for (const meter of workflow.meters ?? []) keys.add(meter);
|
|
3011
|
+
for (const meter of Object.keys(workflow.estimates ?? {}))
|
|
3012
|
+
keys.add(meter);
|
|
3013
|
+
}
|
|
2862
3014
|
return keys;
|
|
2863
3015
|
}
|
|
2864
3016
|
buildRoute(match, options) {
|
|
@@ -2883,11 +3035,6 @@ var Business = class {
|
|
|
2883
3035
|
buildRouteMetering(options) {
|
|
2884
3036
|
if (options.unmetered === true) return void 0;
|
|
2885
3037
|
const defaults = {};
|
|
2886
|
-
if (options.inheritDefaultMeters !== false) {
|
|
2887
|
-
for (const cost of this.defaultMeterCosts) {
|
|
2888
|
-
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
3038
|
for (const cost of this.normalizeMeterCosts(options.costs)) {
|
|
2892
3039
|
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2893
3040
|
}
|
|
@@ -2915,6 +3062,32 @@ var Business = class {
|
|
|
2915
3062
|
out.onStatusCodes = options.onStatusCodes;
|
|
2916
3063
|
return Object.keys(out).length ? out : void 0;
|
|
2917
3064
|
}
|
|
3065
|
+
materializeFeatureFiles() {
|
|
3066
|
+
return this.graph.sortedValues("feature", (file) => file.feature).map((file) => ({
|
|
3067
|
+
...file,
|
|
3068
|
+
routes: file.routes.map((route) => this.materializeRoute(route))
|
|
3069
|
+
}));
|
|
3070
|
+
}
|
|
3071
|
+
materializeRoute(route) {
|
|
3072
|
+
if (route.unmetered === true) return route;
|
|
3073
|
+
const defaults = {
|
|
3074
|
+
...route.metering?.defaults ?? {}
|
|
3075
|
+
};
|
|
3076
|
+
if (route.inheritDefaultMeters !== false) {
|
|
3077
|
+
for (const cost of this.defaultMeterCosts) {
|
|
3078
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
const hasMetering = route.metering !== void 0 || Object.keys(defaults).length > 0;
|
|
3082
|
+
const metering = hasMetering ? {
|
|
3083
|
+
...route.metering ?? {},
|
|
3084
|
+
...Object.keys(defaults).length ? { defaults } : {}
|
|
3085
|
+
} : void 0;
|
|
3086
|
+
return {
|
|
3087
|
+
...route,
|
|
3088
|
+
...metering ? { metering } : {}
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
2918
3091
|
normalizeMeterCost(cost) {
|
|
2919
3092
|
if (!cost || cost.kind !== "meter_cost") {
|
|
2920
3093
|
throw new ManifestBuilderError(
|
|
@@ -3039,6 +3212,27 @@ var Business = class {
|
|
|
3039
3212
|
policyDependsOn(file) {
|
|
3040
3213
|
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
3041
3214
|
}
|
|
3215
|
+
entitlementDependsOn(entitlement) {
|
|
3216
|
+
const limitDimensions = (entitlement.limits ?? []).map(
|
|
3217
|
+
(limit) => limit.dimension
|
|
3218
|
+
);
|
|
3219
|
+
return [
|
|
3220
|
+
...this.dependenciesFor("capability", entitlement.capabilities),
|
|
3221
|
+
...this.existingDependenciesFor("meter", [
|
|
3222
|
+
...entitlement.meters ?? [],
|
|
3223
|
+
...limitDimensions
|
|
3224
|
+
])
|
|
3225
|
+
];
|
|
3226
|
+
}
|
|
3227
|
+
workflowDependsOn(workflow) {
|
|
3228
|
+
return [
|
|
3229
|
+
...this.dependenciesFor("capability", workflow.capabilities),
|
|
3230
|
+
...this.dependenciesFor("meter", [
|
|
3231
|
+
...workflow.meters ?? [],
|
|
3232
|
+
...Object.keys(workflow.estimates ?? {})
|
|
3233
|
+
])
|
|
3234
|
+
];
|
|
3235
|
+
}
|
|
3042
3236
|
featureDependsOn(file) {
|
|
3043
3237
|
const meterKeys = file.routes.flatMap(
|
|
3044
3238
|
(route) => this.routeMeterDependencyKeys(route)
|
|
@@ -3061,12 +3255,26 @@ var Business = class {
|
|
|
3061
3255
|
}
|
|
3062
3256
|
return [...keys];
|
|
3063
3257
|
}
|
|
3064
|
-
assertRouteMeteringValid() {
|
|
3065
|
-
|
|
3258
|
+
assertRouteMeteringValid(files) {
|
|
3259
|
+
const declaredMeters = new Set(
|
|
3260
|
+
this.graph.values("meter").map((meter) => meter.key)
|
|
3261
|
+
);
|
|
3262
|
+
for (const file of files) {
|
|
3066
3263
|
file.routes.forEach((route, routeIndex) => {
|
|
3067
3264
|
if (route.unmetered === true) return;
|
|
3068
3265
|
const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
|
|
3266
|
+
for (const meter of defaults) {
|
|
3267
|
+
if (declaredMeters.has(meter)) continue;
|
|
3268
|
+
throw new ManifestBuilderError(
|
|
3269
|
+
`feature "${file.feature}" route ${routeIndex}: fixed meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3069
3272
|
for (const meter of route.metering?.reports ?? []) {
|
|
3273
|
+
if (!declaredMeters.has(meter)) {
|
|
3274
|
+
throw new ManifestBuilderError(
|
|
3275
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3070
3278
|
if (defaults.has(meter)) {
|
|
3071
3279
|
throw new ManifestBuilderError(
|
|
3072
3280
|
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
@@ -3089,6 +3297,12 @@ var Business = class {
|
|
|
3089
3297
|
);
|
|
3090
3298
|
}
|
|
3091
3299
|
}
|
|
3300
|
+
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
3301
|
+
if (declaredMeters.has(meter)) continue;
|
|
3302
|
+
throw new ManifestBuilderError(
|
|
3303
|
+
`feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3092
3306
|
});
|
|
3093
3307
|
}
|
|
3094
3308
|
}
|
|
@@ -3144,8 +3358,11 @@ var Business = class {
|
|
|
3144
3358
|
return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
|
|
3145
3359
|
}
|
|
3146
3360
|
};
|
|
3147
|
-
function
|
|
3148
|
-
return typeof value === "object" && value !== null && value[
|
|
3361
|
+
function isProduct(value) {
|
|
3362
|
+
return typeof value === "object" && value !== null && value[PRODUCT_BRAND] === true;
|
|
3363
|
+
}
|
|
3364
|
+
function compileProductToManifest(product) {
|
|
3365
|
+
return product[PRODUCT_MANIFEST_COMPILER]();
|
|
3149
3366
|
}
|
|
3150
3367
|
function isPlainObject(value) {
|
|
3151
3368
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3154,7 +3371,7 @@ function buildCustomerContext(options) {
|
|
|
3154
3371
|
return {
|
|
3155
3372
|
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
3156
3373
|
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
3157
|
-
...options.
|
|
3374
|
+
...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
|
|
3158
3375
|
};
|
|
3159
3376
|
}
|
|
3160
3377
|
function deepMerge(base, patch) {
|
|
@@ -3267,16 +3484,16 @@ async function main() {
|
|
|
3267
3484
|
}
|
|
3268
3485
|
const direct = mod.default;
|
|
3269
3486
|
const interop = direct?.default;
|
|
3270
|
-
const candidate =
|
|
3271
|
-
if (!
|
|
3487
|
+
const candidate = isProduct(direct) ? direct : interop;
|
|
3488
|
+
if (!isProduct(candidate)) {
|
|
3272
3489
|
process.stderr.write(
|
|
3273
|
-
`farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the
|
|
3490
|
+
`farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the Product returned by fs.product(\u2026)
|
|
3274
3491
|
`
|
|
3275
3492
|
);
|
|
3276
3493
|
process.exit(1);
|
|
3277
3494
|
}
|
|
3278
3495
|
try {
|
|
3279
|
-
const { ir, irHash } = candidate
|
|
3496
|
+
const { ir, irHash } = compileProductToManifest(candidate);
|
|
3280
3497
|
await writeFile(
|
|
3281
3498
|
resolve(process.cwd(), args.out),
|
|
3282
3499
|
`${JSON.stringify({ ir, irHash }, null, 2)}
|