@farthershore/product 0.3.1 → 0.4.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 +241 -30
- package/dist/codegen.js +43 -16
- package/dist/index.js +246 -37
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/index.d.ts +4 -6
- package/dist/types/ir-types.d.ts +45 -0
- package/dist/types/{business.d.ts → product.d.ts} +73 -29
- 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,10 @@ function hashIr(ir) {
|
|
|
2353
2423
|
}
|
|
2354
2424
|
|
|
2355
2425
|
// src/version.ts
|
|
2356
|
-
var SDK_VERSION = true ? "0.
|
|
2426
|
+
var SDK_VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
2357
2427
|
|
|
2358
|
-
// src/
|
|
2359
|
-
var
|
|
2428
|
+
// src/product.ts
|
|
2429
|
+
var PRODUCT_BRAND = Symbol.for("farthershore.product.product");
|
|
2360
2430
|
function isCapabilityGrant(value) {
|
|
2361
2431
|
return typeof value === "object" && value !== null && value.kind === "capability_grant";
|
|
2362
2432
|
}
|
|
@@ -2401,8 +2471,8 @@ function parseRouteMatch(match) {
|
|
|
2401
2471
|
}
|
|
2402
2472
|
return { method, path };
|
|
2403
2473
|
}
|
|
2404
|
-
var
|
|
2405
|
-
[
|
|
2474
|
+
var Product = class {
|
|
2475
|
+
[PRODUCT_BRAND] = true;
|
|
2406
2476
|
name;
|
|
2407
2477
|
options;
|
|
2408
2478
|
graph = new ManifestResourceGraph();
|
|
@@ -2413,15 +2483,16 @@ var Business = class {
|
|
|
2413
2483
|
api;
|
|
2414
2484
|
frontend;
|
|
2415
2485
|
lifecycle;
|
|
2486
|
+
offering;
|
|
2416
2487
|
/** Escape hatches — raw platform-schema JSON, validated at toIR(). */
|
|
2417
2488
|
raw;
|
|
2418
2489
|
constructor(name, options) {
|
|
2419
2490
|
if (!name || typeof name !== "string") {
|
|
2420
|
-
throw new ManifestBuilderError("fs.
|
|
2491
|
+
throw new ManifestBuilderError("fs.product(name, \u2026): name is required");
|
|
2421
2492
|
}
|
|
2422
|
-
if (!options?.
|
|
2493
|
+
if (!options?.origin) {
|
|
2423
2494
|
throw new ManifestBuilderError(
|
|
2424
|
-
`fs.
|
|
2495
|
+
`fs.product("${name}", \u2026): options.origin is required (the business logic origin Farther Shore calls for customer-facing actions)`
|
|
2425
2496
|
);
|
|
2426
2497
|
}
|
|
2427
2498
|
this.name = name;
|
|
@@ -2436,7 +2507,7 @@ var Business = class {
|
|
|
2436
2507
|
const file = this.getFeatureFile(featureKey);
|
|
2437
2508
|
if (!file) {
|
|
2438
2509
|
throw new ManifestBuilderError(
|
|
2439
|
-
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call
|
|
2510
|
+
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call product.feature("${featureKey}", \u2026) first`
|
|
2440
2511
|
);
|
|
2441
2512
|
}
|
|
2442
2513
|
file.routes.push(this.buildRoute(match, options2));
|
|
@@ -2529,6 +2600,9 @@ var Business = class {
|
|
|
2529
2600
|
return this;
|
|
2530
2601
|
}
|
|
2531
2602
|
};
|
|
2603
|
+
this.offering = {
|
|
2604
|
+
plan: (key, options2) => this.plan(key, options2)
|
|
2605
|
+
};
|
|
2532
2606
|
this.raw = {
|
|
2533
2607
|
productPatch: (patch) => {
|
|
2534
2608
|
this.productPatch = deepMerge(this.productPatch, patch);
|
|
@@ -2699,6 +2773,57 @@ var Business = class {
|
|
|
2699
2773
|
this.graph.register("policy", name, file, this.policyDependsOn(file));
|
|
2700
2774
|
return { kind: "policy", key: name };
|
|
2701
2775
|
}
|
|
2776
|
+
surface(type, options = {}) {
|
|
2777
|
+
const key = options.key ?? type;
|
|
2778
|
+
this.assertNewKey("surface", key, "surface");
|
|
2779
|
+
const surface = {
|
|
2780
|
+
key,
|
|
2781
|
+
type,
|
|
2782
|
+
...options.display !== void 0 ? { display: options.display } : {},
|
|
2783
|
+
...options.description !== void 0 ? { description: options.description } : {}
|
|
2784
|
+
};
|
|
2785
|
+
this.graph.register("surface", key, surface);
|
|
2786
|
+
return { kind: "surface", key };
|
|
2787
|
+
}
|
|
2788
|
+
workflow(key, options = {}) {
|
|
2789
|
+
this.assertNewKey("workflow", key, "workflow");
|
|
2790
|
+
const workflow = {
|
|
2791
|
+
key,
|
|
2792
|
+
kind: options.kind ?? "async_job",
|
|
2793
|
+
trigger: options.trigger ?? { type: "manual" },
|
|
2794
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
2795
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2796
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2797
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
|
|
2798
|
+
...options.estimates !== void 0 ? { estimates: options.estimates } : {},
|
|
2799
|
+
...options.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
2800
|
+
};
|
|
2801
|
+
this.graph.register(
|
|
2802
|
+
"workflow",
|
|
2803
|
+
key,
|
|
2804
|
+
workflow,
|
|
2805
|
+
this.workflowDependsOn(workflow)
|
|
2806
|
+
);
|
|
2807
|
+
return { kind: "workflow", key };
|
|
2808
|
+
}
|
|
2809
|
+
entitlement(key, options = {}) {
|
|
2810
|
+
this.assertNewKey("entitlement", key, "entitlement");
|
|
2811
|
+
const entitlement = {
|
|
2812
|
+
key,
|
|
2813
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2814
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2815
|
+
...options.featureGates !== void 0 ? { featureGates: options.featureGates } : {},
|
|
2816
|
+
...options.limits?.length ? { limits: options.limits } : {},
|
|
2817
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {}
|
|
2818
|
+
};
|
|
2819
|
+
this.graph.register(
|
|
2820
|
+
"entitlement",
|
|
2821
|
+
key,
|
|
2822
|
+
entitlement,
|
|
2823
|
+
this.entitlementDependsOn(entitlement)
|
|
2824
|
+
);
|
|
2825
|
+
return { kind: "entitlement", key };
|
|
2826
|
+
}
|
|
2702
2827
|
plan(key, options) {
|
|
2703
2828
|
this.assertNewKey("plan", key, "plan");
|
|
2704
2829
|
const capabilityKeys = (options.capabilities ?? []).map(keyOf);
|
|
@@ -2765,16 +2890,14 @@ var Business = class {
|
|
|
2765
2890
|
/** Assemble + validate the Manifest IR. Throws ManifestValidationError
|
|
2766
2891
|
* with structured issues when the declared state is invalid. */
|
|
2767
2892
|
toIR() {
|
|
2893
|
+
const routes = this.materializeFeatureFiles();
|
|
2894
|
+
this.assertRouteMeteringValid(routes);
|
|
2768
2895
|
this.assertGraphDependenciesSatisfied();
|
|
2769
|
-
this.assertRouteMeteringValid();
|
|
2770
2896
|
const candidate = {
|
|
2771
2897
|
irVersion: 1,
|
|
2772
2898
|
sdkVersion: SDK_VERSION,
|
|
2773
2899
|
product: this.buildProductSpec(),
|
|
2774
|
-
routes
|
|
2775
|
-
"feature",
|
|
2776
|
-
(file) => file.feature
|
|
2777
|
-
),
|
|
2900
|
+
routes,
|
|
2778
2901
|
policies: this.graph.sortedValues(
|
|
2779
2902
|
"policy",
|
|
2780
2903
|
(file) => file.name
|
|
@@ -2794,10 +2917,10 @@ var Business = class {
|
|
|
2794
2917
|
const base = {
|
|
2795
2918
|
product: {
|
|
2796
2919
|
name: this.name,
|
|
2797
|
-
baseUrl: options.
|
|
2920
|
+
baseUrl: options.origin,
|
|
2798
2921
|
...options.displayName !== void 0 ? { displayName: options.displayName } : {},
|
|
2799
2922
|
...options.description !== void 0 ? { description: options.description } : {},
|
|
2800
|
-
...options.
|
|
2923
|
+
...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
|
|
2801
2924
|
...options.visibility !== void 0 ? { visibility: options.visibility } : {},
|
|
2802
2925
|
...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
|
|
2803
2926
|
...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
|
|
@@ -2814,6 +2937,24 @@ var Business = class {
|
|
|
2814
2937
|
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2815
2938
|
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
2816
2939
|
...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
|
|
2940
|
+
...this.graph.values("surface").length ? {
|
|
2941
|
+
surfaces: this.graph.sortedValues(
|
|
2942
|
+
"surface",
|
|
2943
|
+
(surface) => surface.key
|
|
2944
|
+
)
|
|
2945
|
+
} : {},
|
|
2946
|
+
...this.graph.values("entitlement").length ? {
|
|
2947
|
+
entitlements: this.graph.sortedValues(
|
|
2948
|
+
"entitlement",
|
|
2949
|
+
(entitlement) => entitlement.key
|
|
2950
|
+
)
|
|
2951
|
+
} : {},
|
|
2952
|
+
...this.graph.values("workflow").length ? {
|
|
2953
|
+
workflows: this.graph.sortedValues(
|
|
2954
|
+
"workflow",
|
|
2955
|
+
(workflow) => workflow.key
|
|
2956
|
+
)
|
|
2957
|
+
} : {},
|
|
2817
2958
|
...this.graph.has("frontend", "manifest") ? {
|
|
2818
2959
|
frontend: this.graph.get(
|
|
2819
2960
|
"frontend",
|
|
@@ -2849,6 +2990,9 @@ var Business = class {
|
|
|
2849
2990
|
}
|
|
2850
2991
|
routeValueMeterKeys() {
|
|
2851
2992
|
const keys = /* @__PURE__ */ new Set();
|
|
2993
|
+
for (const cost of this.defaultMeterCosts) {
|
|
2994
|
+
keys.add(cost.meter);
|
|
2995
|
+
}
|
|
2852
2996
|
for (const file of this.graph.values("feature")) {
|
|
2853
2997
|
for (const route of file.routes) {
|
|
2854
2998
|
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
@@ -2859,6 +3003,11 @@ var Business = class {
|
|
|
2859
3003
|
}
|
|
2860
3004
|
}
|
|
2861
3005
|
}
|
|
3006
|
+
for (const workflow of this.graph.values("workflow")) {
|
|
3007
|
+
for (const meter of workflow.meters ?? []) keys.add(meter);
|
|
3008
|
+
for (const meter of Object.keys(workflow.estimates ?? {}))
|
|
3009
|
+
keys.add(meter);
|
|
3010
|
+
}
|
|
2862
3011
|
return keys;
|
|
2863
3012
|
}
|
|
2864
3013
|
buildRoute(match, options) {
|
|
@@ -2883,11 +3032,6 @@ var Business = class {
|
|
|
2883
3032
|
buildRouteMetering(options) {
|
|
2884
3033
|
if (options.unmetered === true) return void 0;
|
|
2885
3034
|
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
3035
|
for (const cost of this.normalizeMeterCosts(options.costs)) {
|
|
2892
3036
|
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2893
3037
|
}
|
|
@@ -2915,6 +3059,32 @@ var Business = class {
|
|
|
2915
3059
|
out.onStatusCodes = options.onStatusCodes;
|
|
2916
3060
|
return Object.keys(out).length ? out : void 0;
|
|
2917
3061
|
}
|
|
3062
|
+
materializeFeatureFiles() {
|
|
3063
|
+
return this.graph.sortedValues("feature", (file) => file.feature).map((file) => ({
|
|
3064
|
+
...file,
|
|
3065
|
+
routes: file.routes.map((route) => this.materializeRoute(route))
|
|
3066
|
+
}));
|
|
3067
|
+
}
|
|
3068
|
+
materializeRoute(route) {
|
|
3069
|
+
if (route.unmetered === true) return route;
|
|
3070
|
+
const defaults = {
|
|
3071
|
+
...route.metering?.defaults ?? {}
|
|
3072
|
+
};
|
|
3073
|
+
if (route.inheritDefaultMeters !== false) {
|
|
3074
|
+
for (const cost of this.defaultMeterCosts) {
|
|
3075
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
const hasMetering = route.metering !== void 0 || Object.keys(defaults).length > 0;
|
|
3079
|
+
const metering = hasMetering ? {
|
|
3080
|
+
...route.metering ?? {},
|
|
3081
|
+
...Object.keys(defaults).length ? { defaults } : {}
|
|
3082
|
+
} : void 0;
|
|
3083
|
+
return {
|
|
3084
|
+
...route,
|
|
3085
|
+
...metering ? { metering } : {}
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
2918
3088
|
normalizeMeterCost(cost) {
|
|
2919
3089
|
if (!cost || cost.kind !== "meter_cost") {
|
|
2920
3090
|
throw new ManifestBuilderError(
|
|
@@ -3039,6 +3209,27 @@ var Business = class {
|
|
|
3039
3209
|
policyDependsOn(file) {
|
|
3040
3210
|
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
3041
3211
|
}
|
|
3212
|
+
entitlementDependsOn(entitlement) {
|
|
3213
|
+
const limitDimensions = (entitlement.limits ?? []).map(
|
|
3214
|
+
(limit) => limit.dimension
|
|
3215
|
+
);
|
|
3216
|
+
return [
|
|
3217
|
+
...this.dependenciesFor("capability", entitlement.capabilities),
|
|
3218
|
+
...this.existingDependenciesFor("meter", [
|
|
3219
|
+
...entitlement.meters ?? [],
|
|
3220
|
+
...limitDimensions
|
|
3221
|
+
])
|
|
3222
|
+
];
|
|
3223
|
+
}
|
|
3224
|
+
workflowDependsOn(workflow) {
|
|
3225
|
+
return [
|
|
3226
|
+
...this.dependenciesFor("capability", workflow.capabilities),
|
|
3227
|
+
...this.dependenciesFor("meter", [
|
|
3228
|
+
...workflow.meters ?? [],
|
|
3229
|
+
...Object.keys(workflow.estimates ?? {})
|
|
3230
|
+
])
|
|
3231
|
+
];
|
|
3232
|
+
}
|
|
3042
3233
|
featureDependsOn(file) {
|
|
3043
3234
|
const meterKeys = file.routes.flatMap(
|
|
3044
3235
|
(route) => this.routeMeterDependencyKeys(route)
|
|
@@ -3061,12 +3252,26 @@ var Business = class {
|
|
|
3061
3252
|
}
|
|
3062
3253
|
return [...keys];
|
|
3063
3254
|
}
|
|
3064
|
-
assertRouteMeteringValid() {
|
|
3065
|
-
|
|
3255
|
+
assertRouteMeteringValid(files) {
|
|
3256
|
+
const declaredMeters = new Set(
|
|
3257
|
+
this.graph.values("meter").map((meter) => meter.key)
|
|
3258
|
+
);
|
|
3259
|
+
for (const file of files) {
|
|
3066
3260
|
file.routes.forEach((route, routeIndex) => {
|
|
3067
3261
|
if (route.unmetered === true) return;
|
|
3068
3262
|
const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
|
|
3263
|
+
for (const meter of defaults) {
|
|
3264
|
+
if (declaredMeters.has(meter)) continue;
|
|
3265
|
+
throw new ManifestBuilderError(
|
|
3266
|
+
`feature "${file.feature}" route ${routeIndex}: fixed meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3069
3269
|
for (const meter of route.metering?.reports ?? []) {
|
|
3270
|
+
if (!declaredMeters.has(meter)) {
|
|
3271
|
+
throw new ManifestBuilderError(
|
|
3272
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3273
|
+
);
|
|
3274
|
+
}
|
|
3070
3275
|
if (defaults.has(meter)) {
|
|
3071
3276
|
throw new ManifestBuilderError(
|
|
3072
3277
|
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
@@ -3089,6 +3294,12 @@ var Business = class {
|
|
|
3089
3294
|
);
|
|
3090
3295
|
}
|
|
3091
3296
|
}
|
|
3297
|
+
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
3298
|
+
if (declaredMeters.has(meter)) continue;
|
|
3299
|
+
throw new ManifestBuilderError(
|
|
3300
|
+
`feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3092
3303
|
});
|
|
3093
3304
|
}
|
|
3094
3305
|
}
|
|
@@ -3144,8 +3355,8 @@ var Business = class {
|
|
|
3144
3355
|
return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
|
|
3145
3356
|
}
|
|
3146
3357
|
};
|
|
3147
|
-
function
|
|
3148
|
-
return typeof value === "object" && value !== null && value[
|
|
3358
|
+
function isProduct(value) {
|
|
3359
|
+
return typeof value === "object" && value !== null && value[PRODUCT_BRAND] === true;
|
|
3149
3360
|
}
|
|
3150
3361
|
function isPlainObject(value) {
|
|
3151
3362
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3154,7 +3365,7 @@ function buildCustomerContext(options) {
|
|
|
3154
3365
|
return {
|
|
3155
3366
|
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
3156
3367
|
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
3157
|
-
...options.
|
|
3368
|
+
...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
|
|
3158
3369
|
};
|
|
3159
3370
|
}
|
|
3160
3371
|
function deepMerge(base, patch) {
|
|
@@ -3267,10 +3478,10 @@ async function main() {
|
|
|
3267
3478
|
}
|
|
3268
3479
|
const direct = mod.default;
|
|
3269
3480
|
const interop = direct?.default;
|
|
3270
|
-
const candidate =
|
|
3271
|
-
if (!
|
|
3481
|
+
const candidate = isProduct(direct) ? direct : interop;
|
|
3482
|
+
if (!isProduct(candidate)) {
|
|
3272
3483
|
process.stderr.write(
|
|
3273
|
-
`farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the
|
|
3484
|
+
`farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the Product returned by fs.product(\u2026)
|
|
3274
3485
|
`
|
|
3275
3486
|
);
|
|
3276
3487
|
process.exit(1);
|