@farthershore/product 0.5.0 → 0.6.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 +7 -12
- package/dist/bin.js +1739 -1580
- package/dist/codegen.js +0 -1
- package/dist/index.js +1744 -1585
- package/dist/types/backend.d.ts +41 -0
- package/dist/types/declarations.d.ts +121 -0
- package/dist/types/dependencies.d.ts +13 -0
- package/dist/types/frontend.d.ts +23 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/ir-types.d.ts +36 -12
- package/dist/types/product-assembly.d.ts +22 -0
- package/dist/types/product.d.ts +30 -68
- package/dist/types/refs.d.ts +11 -0
- package/dist/types/resource-graph.d.ts +1 -1
- package/dist/types/route-metering.d.ts +60 -0
- package/dist/types/validate.d.ts +2 -4
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -679,7 +679,7 @@ var environmentOverrideBlockSchema = z8.object({
|
|
|
679
679
|
var environmentsBlockSchema = z8.record(z8.string().min(1).max(64), environmentOverrideBlockSchema);
|
|
680
680
|
|
|
681
681
|
// ../contracts/dist/plans/spec/product.js
|
|
682
|
-
import { z as
|
|
682
|
+
import { z as z18 } from "zod";
|
|
683
683
|
|
|
684
684
|
// ../contracts/dist/plans/spec/frontend-layer.js
|
|
685
685
|
import { z as z9 } from "zod";
|
|
@@ -924,46 +924,406 @@ var subscriptionAddOnSchema = z12.object({
|
|
|
924
924
|
stripeSubscriptionItemId: z12.string().optional()
|
|
925
925
|
});
|
|
926
926
|
|
|
927
|
-
// ../contracts/dist/plans/spec/
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
927
|
+
// ../contracts/dist/plans/spec/backend-layer.js
|
|
928
|
+
import { z as z13 } from "zod";
|
|
929
|
+
var BACKEND_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
930
|
+
var backendIdSchema = z13.string().min(1).max(64).regex(BACKEND_ID_PATTERN, "backend id must be a lowercase slug ([a-z0-9][a-z0-9_-]*)");
|
|
931
|
+
var backendTransportModeSchema = z13.enum([
|
|
932
|
+
"public_origin",
|
|
933
|
+
"mtls",
|
|
934
|
+
"cloudflare_tunnel"
|
|
935
|
+
]);
|
|
936
|
+
var backendTransportRunnerSchema = z13.enum([
|
|
937
|
+
"managed_cloudflared",
|
|
938
|
+
"sidecar"
|
|
939
|
+
]);
|
|
940
|
+
var backendTransportSchema = z13.object({
|
|
941
|
+
mode: backendTransportModeSchema.default("public_origin"),
|
|
942
|
+
runner: backendTransportRunnerSchema.optional()
|
|
943
|
+
}).strict();
|
|
944
|
+
var backendVerificationSchema = z13.object({
|
|
945
|
+
required: z13.boolean().default(false)
|
|
946
|
+
}).strict();
|
|
947
|
+
var backendDefinitionSchema = z13.object({
|
|
948
|
+
/** Human-friendly label. Defaults to the id when omitted. */
|
|
949
|
+
name: z13.string().min(1).max(120).optional(),
|
|
950
|
+
/** Stable slug for the backend (origin-hostname / token scoping). Defaults
|
|
951
|
+
* to the id when omitted. */
|
|
952
|
+
slug: backendIdSchema.optional(),
|
|
953
|
+
transport: backendTransportSchema.default({ mode: "public_origin" }),
|
|
954
|
+
verification: backendVerificationSchema.default({ required: false }),
|
|
955
|
+
/** Meter allow-list. Omitted = all product meters allowed. */
|
|
956
|
+
meters: z13.array(z13.string().min(1).max(64)).max(100).optional(),
|
|
957
|
+
/** Marks the default backend when a product declares more than one. At
|
|
958
|
+
* most one backend may set this (compiler enforces / AMBIGUOUS_DEFAULT). */
|
|
959
|
+
default: z13.boolean().optional(),
|
|
960
|
+
/** Reachable origin for `public_origin` / `mtls`. */
|
|
961
|
+
originUrl: z13.string().url().optional(),
|
|
962
|
+
/** Access-protected `*.fs-origin` host for `cloudflare_tunnel`. */
|
|
963
|
+
originHostname: z13.string().min(1).max(255).optional()
|
|
964
|
+
}).strict();
|
|
965
|
+
var productBackendBlockSchema = z13.record(backendIdSchema, backendDefinitionSchema);
|
|
966
|
+
var routeBackendBindingSchema = backendIdSchema;
|
|
967
|
+
var BACKEND_DIAGNOSTIC_CODES = {
|
|
968
|
+
unknownBackendInRoute: "UNKNOWN_BACKEND_IN_ROUTE",
|
|
969
|
+
ambiguousDefaultBackend: "AMBIGUOUS_DEFAULT_BACKEND",
|
|
970
|
+
routeMeterNotAllowedByBackend: "ROUTE_METER_NOT_ALLOWED_BY_BACKEND",
|
|
971
|
+
unknownMeterInBackend: "UNKNOWN_METER_IN_BACKEND"
|
|
972
|
+
};
|
|
973
|
+
function resolveDefaultBackendId(backends) {
|
|
974
|
+
const ids = Object.keys(backends ?? {});
|
|
975
|
+
if (ids.length === 0)
|
|
976
|
+
return { defaultId: null, ambiguous: false };
|
|
977
|
+
if (ids.length === 1)
|
|
978
|
+
return { defaultId: ids[0], ambiguous: false };
|
|
979
|
+
const explicit = ids.filter((id) => backends[id]?.default === true);
|
|
980
|
+
if (explicit.length === 1) {
|
|
981
|
+
return { defaultId: explicit[0], ambiguous: false };
|
|
982
|
+
}
|
|
983
|
+
return { defaultId: null, ambiguous: true };
|
|
936
984
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
985
|
+
|
|
986
|
+
// ../contracts/dist/plans/spec/routes-layer.js
|
|
987
|
+
import { z as z17 } from "zod";
|
|
988
|
+
|
|
989
|
+
// ../contracts/dist/plans/spec/policies-layer.js
|
|
990
|
+
import { z as z15 } from "zod";
|
|
991
|
+
|
|
992
|
+
// ../contracts/dist/plans/spec/policy-types.js
|
|
993
|
+
import { z as z14 } from "zod";
|
|
994
|
+
var rateLimitWindowSchema = z14.string().min(2).max(20).regex(/^\d+(ms|s|m|h)$/, "rate_limit window must look like `60s`, `5m`, `1h`");
|
|
995
|
+
var rateLimitConfigSchema = z14.object({
|
|
996
|
+
strategy: z14.enum(["token_bucket", "sliding_window", "fixed_window"]).default("token_bucket"),
|
|
997
|
+
/**
|
|
998
|
+
* Which request dimensions identify the bucket. v0.3.0 supports a
|
|
999
|
+
* fixed set; extending requires a coordinated gateway/policy-engine
|
|
1000
|
+
* change. The `subscription` dimension is the steady-state default;
|
|
1001
|
+
* `ip` is for unauthenticated probes; `credential` is finer-grained
|
|
1002
|
+
* than subscription (per-key throttling).
|
|
1003
|
+
*/
|
|
1004
|
+
dimensions: z14.array(z14.enum(["subscription", "credential", "ip", "route"])).min(1).max(4).default(["subscription"]),
|
|
1005
|
+
limits: z14.array(z14.object({
|
|
1006
|
+
window: rateLimitWindowSchema,
|
|
1007
|
+
max: z14.number().int().positive().max(1e7)
|
|
1008
|
+
})).min(1).max(10),
|
|
1009
|
+
/**
|
|
1010
|
+
* Bounded fail-open behaviour for DO outages. See architecture RFC
|
|
1011
|
+
* "Fail-open guardrails" section. When the policy executor observes
|
|
1012
|
+
* `max_consecutive_failures` DO-call failures within
|
|
1013
|
+
* `max_window_seconds`, it transitions to `degraded_mode` until
|
|
1014
|
+
* `recovery_threshold` consecutive successes restore normal
|
|
1015
|
+
* evaluation.
|
|
1016
|
+
*/
|
|
1017
|
+
fail_open: z14.object({
|
|
1018
|
+
max_consecutive_failures: z14.number().int().positive().default(100),
|
|
1019
|
+
max_window_seconds: z14.number().int().positive().default(60),
|
|
1020
|
+
recovery_threshold: z14.number().int().positive().default(50),
|
|
1021
|
+
degraded_mode: z14.enum([
|
|
1022
|
+
"safe_mode_block",
|
|
1023
|
+
"safe_mode_throttle",
|
|
1024
|
+
"runtime_killswitch_trigger"
|
|
1025
|
+
]).default("safe_mode_throttle")
|
|
1026
|
+
}).default({
|
|
1027
|
+
max_consecutive_failures: 100,
|
|
1028
|
+
max_window_seconds: 60,
|
|
1029
|
+
recovery_threshold: 50,
|
|
1030
|
+
degraded_mode: "safe_mode_throttle"
|
|
1031
|
+
})
|
|
1032
|
+
});
|
|
1033
|
+
var authConfigSchema = z14.object({
|
|
1034
|
+
header_name: z14.string().min(1).max(100).default("x-api-key"),
|
|
1035
|
+
/**
|
|
1036
|
+
* How the gateway constructs the upstream Authorization header:
|
|
1037
|
+
* - `none` → no upstream auth header added
|
|
1038
|
+
* - `static_bearer` → forward a configured static token
|
|
1039
|
+
* - `subscriber_jwt` → mint a per-subscriber JWT (out of scope v0.3.0)
|
|
1040
|
+
*/
|
|
1041
|
+
upstream_token_source: z14.discriminatedUnion("type", [
|
|
1042
|
+
z14.object({ type: z14.literal("none") }),
|
|
1043
|
+
z14.object({
|
|
1044
|
+
type: z14.literal("static_bearer"),
|
|
1045
|
+
token_secret_ref: z14.string().min(1).max(200).describe("Reference into the secret store (e.g. CF Secret name); not the raw token")
|
|
1046
|
+
})
|
|
1047
|
+
]).default({ type: "none" }),
|
|
1048
|
+
/**
|
|
1049
|
+
* When `true`, treat the inbound credential's scopes (if any) as
|
|
1050
|
+
* additional gating beyond the entitlement check. v0.3.0 ships with
|
|
1051
|
+
* `strict` as the default.
|
|
1052
|
+
*/
|
|
1053
|
+
scope_mode: z14.enum(["strict", "advisory", "off"]).default("strict")
|
|
1054
|
+
});
|
|
1055
|
+
var concurrencyConfigSchema = z14.object({
|
|
1056
|
+
max_in_flight: z14.number().int().positive().max(1e4),
|
|
1057
|
+
/**
|
|
1058
|
+
* Which dimensions key the lease bucket. Matches the existing
|
|
1059
|
+
* ConcurrencyLease DO `idFromName` pattern (subscription | capability
|
|
1060
|
+
* tuple).
|
|
1061
|
+
*/
|
|
1062
|
+
dimensions: z14.array(z14.enum(["subscription", "credential", "capability"])).min(1).max(3).default(["subscription"]),
|
|
1063
|
+
/**
|
|
1064
|
+
* Optional capability scope. When set, the lease bucket is keyed
|
|
1065
|
+
* partly by this capability name — separate buckets per capability.
|
|
1066
|
+
*/
|
|
1067
|
+
capability: z14.string().min(1).max(120).optional(),
|
|
1068
|
+
/**
|
|
1069
|
+
* Lease TTL — releases automatically after this many seconds even if
|
|
1070
|
+
* the request never returns (defensive default 30s, mirrors existing
|
|
1071
|
+
* ConcurrencyLease behaviour).
|
|
1072
|
+
*/
|
|
1073
|
+
lease_ttl_seconds: z14.number().int().positive().max(600).default(30),
|
|
1074
|
+
fail_open: z14.object({
|
|
1075
|
+
max_consecutive_failures: z14.number().int().positive().default(50),
|
|
1076
|
+
max_window_seconds: z14.number().int().positive().default(60),
|
|
1077
|
+
recovery_threshold: z14.number().int().positive().default(20),
|
|
1078
|
+
degraded_mode: z14.enum([
|
|
1079
|
+
"safe_mode_block",
|
|
1080
|
+
"safe_mode_throttle",
|
|
1081
|
+
"runtime_killswitch_trigger"
|
|
1082
|
+
]).default("safe_mode_throttle")
|
|
1083
|
+
}).default({
|
|
1084
|
+
max_consecutive_failures: 50,
|
|
1085
|
+
max_window_seconds: 60,
|
|
1086
|
+
recovery_threshold: 20,
|
|
1087
|
+
degraded_mode: "safe_mode_throttle"
|
|
1088
|
+
})
|
|
1089
|
+
});
|
|
1090
|
+
var retryConfigSchema = z14.object({
|
|
1091
|
+
max_attempts: z14.number().int().min(1).max(5).default(2),
|
|
1092
|
+
/**
|
|
1093
|
+
* HTTP status codes that trigger a retry. 5xx is the default; opt
|
|
1094
|
+
* into 429 retries only when the upstream understands `Retry-After`.
|
|
1095
|
+
*/
|
|
1096
|
+
retry_on_status: z14.array(z14.number().int().min(400).max(599)).min(1).max(20).default([502, 503, 504]),
|
|
1097
|
+
/**
|
|
1098
|
+
* Backoff curve. Total wall-clock attempt time is bounded so the
|
|
1099
|
+
* gateway worker cannot block past `total_budget_ms`.
|
|
1100
|
+
*/
|
|
1101
|
+
backoff: z14.object({
|
|
1102
|
+
initial_ms: z14.number().int().positive().max(5e3).default(100),
|
|
1103
|
+
multiplier: z14.number().positive().max(10).default(2),
|
|
1104
|
+
jitter: z14.enum(["none", "full", "equal"]).default("equal"),
|
|
1105
|
+
total_budget_ms: z14.number().int().positive().max(3e4).default(5e3)
|
|
1106
|
+
}).default({
|
|
1107
|
+
initial_ms: 100,
|
|
1108
|
+
multiplier: 2,
|
|
1109
|
+
jitter: "equal",
|
|
1110
|
+
total_budget_ms: 5e3
|
|
1111
|
+
})
|
|
1112
|
+
});
|
|
1113
|
+
var transformConfigSchema = z14.object({
|
|
1114
|
+
/**
|
|
1115
|
+
* When the transform applies. `request` runs before upstream forward;
|
|
1116
|
+
* `response` runs after. Most transforms are one or the other; both
|
|
1117
|
+
* is rare.
|
|
1118
|
+
*/
|
|
1119
|
+
applies_to: z14.enum(["request", "response", "both"]).default("request"),
|
|
1120
|
+
/**
|
|
1121
|
+
* List of key rewrites. Source path uses dot notation (`a.b.c`);
|
|
1122
|
+
* `target` may include the same syntax to move keys around. Drops
|
|
1123
|
+
* are expressed as `target: null`.
|
|
1124
|
+
*/
|
|
1125
|
+
rewrites: z14.array(z14.object({
|
|
1126
|
+
source: z14.string().min(1).max(200),
|
|
1127
|
+
target: z14.string().min(1).max(200).nullable()
|
|
1128
|
+
})).min(1).max(20)
|
|
1129
|
+
});
|
|
1130
|
+
var policyBodySchema = z14.discriminatedUnion("type", [
|
|
1131
|
+
z14.object({ type: z14.literal("rate_limit"), config: rateLimitConfigSchema }),
|
|
1132
|
+
z14.object({ type: z14.literal("auth"), config: authConfigSchema }),
|
|
1133
|
+
z14.object({ type: z14.literal("concurrency"), config: concurrencyConfigSchema }),
|
|
1134
|
+
z14.object({ type: z14.literal("retry"), config: retryConfigSchema }),
|
|
1135
|
+
z14.object({ type: z14.literal("transform"), config: transformConfigSchema })
|
|
1136
|
+
]);
|
|
1137
|
+
|
|
1138
|
+
// ../contracts/dist/plans/spec/policies-layer.js
|
|
1139
|
+
var cacheProfileSchema = z15.enum(["long", "short", "blocking"]).default("long");
|
|
1140
|
+
var policyCompatibilitySchema = z15.object({
|
|
1141
|
+
route_types: z15.array(z15.enum(["http"])).max(5).optional(),
|
|
1142
|
+
meters: z15.array(z15.string().min(1).max(64)).max(20).optional(),
|
|
1143
|
+
auth_modes: z15.array(z15.enum(["api_key", "oauth2", "anonymous"])).max(5).optional()
|
|
1144
|
+
});
|
|
1145
|
+
var policyLayerSchema = z15.intersection(z15.object({
|
|
1146
|
+
/**
|
|
1147
|
+
* Policy name. Referenced by routes via `policies: [<name>]`. Must
|
|
1148
|
+
* be unique across the product; the compiler enforces this in the
|
|
1149
|
+
* cross-layer validation pass.
|
|
1150
|
+
*/
|
|
1151
|
+
name: z15.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Policy name must be lowercase alphanumeric with hyphens/underscores"),
|
|
1152
|
+
description: z15.string().max(500).optional(),
|
|
1153
|
+
compatible_with: policyCompatibilitySchema.default({}),
|
|
1154
|
+
/**
|
|
1155
|
+
* Mutation class — runtime vs contractual. Policies are operational
|
|
1156
|
+
* by nature so the default is `runtime`. Marking a policy as
|
|
1157
|
+
* `contractual` signals that changes to it require human approval
|
|
1158
|
+
* (invariant #16).
|
|
1159
|
+
*/
|
|
1160
|
+
mutation_class: z15.enum(["runtime", "contractual"]).default("runtime"),
|
|
1161
|
+
cacheProfile: cacheProfileSchema
|
|
1162
|
+
}), policyBodySchema);
|
|
1163
|
+
|
|
1164
|
+
// ../contracts/dist/framework/actions/index.js
|
|
1165
|
+
import { z as z16 } from "zod";
|
|
1166
|
+
var actionKindSchema = z16.enum(["query", "mutation"]);
|
|
1167
|
+
var actionAuditPolicySchema = z16.enum(["none", "metadata", "full"]);
|
|
1168
|
+
var actionSubjectBindingSchema = z16.object({
|
|
1169
|
+
type: z16.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/),
|
|
1170
|
+
from: z16.enum(["header", "path_param"]),
|
|
1171
|
+
name: z16.string().min(1).max(120)
|
|
1172
|
+
});
|
|
1173
|
+
var actionResourceEffectSchema = z16.object({
|
|
1174
|
+
resource: z16.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/),
|
|
1175
|
+
effect: z16.enum(["create", "delete"])
|
|
1176
|
+
});
|
|
1177
|
+
var actionSpecSchema = z16.object({
|
|
1178
|
+
id: z16.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/),
|
|
1179
|
+
title: z16.string().min(1).max(160).optional(),
|
|
1180
|
+
kind: actionKindSchema,
|
|
1181
|
+
actorType: z16.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/).optional(),
|
|
1182
|
+
subject: actionSubjectBindingSchema.optional(),
|
|
1183
|
+
inputSchemaRef: z16.string().min(1).max(240).optional(),
|
|
1184
|
+
audit: actionAuditPolicySchema.default("metadata"),
|
|
1185
|
+
resource: actionResourceEffectSchema.optional()
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// ../contracts/dist/plans/spec/routes-layer.js
|
|
1189
|
+
var routeMatchSchema = z17.object({
|
|
1190
|
+
method: z17.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
|
|
1191
|
+
path: z17.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]")
|
|
1192
|
+
});
|
|
1193
|
+
function statusPolicyPartIsValid(part) {
|
|
1194
|
+
const trimmed = part.trim();
|
|
1195
|
+
if (!trimmed)
|
|
1196
|
+
return false;
|
|
1197
|
+
const [startRaw, endRaw, extra] = trimmed.split("-");
|
|
1198
|
+
if (extra !== void 0 || !/^\d{3}$/.test(startRaw ?? ""))
|
|
1199
|
+
return false;
|
|
1200
|
+
const start = Number(startRaw);
|
|
1201
|
+
const end = endRaw === void 0 ? start : Number(endRaw);
|
|
1202
|
+
if (endRaw !== void 0 && !/^\d{3}$/.test(endRaw))
|
|
1203
|
+
return false;
|
|
1204
|
+
return start >= 100 && start <= 599 && end >= 100 && end <= 599 && start <= end;
|
|
952
1205
|
}
|
|
953
|
-
function
|
|
954
|
-
|
|
955
|
-
if ((monthly ?? 0) === 0)
|
|
956
|
-
return;
|
|
957
|
-
ctx.addIssue({
|
|
958
|
-
code: "custom",
|
|
959
|
-
path: ["plans", index, "recurring_fee_cents"],
|
|
960
|
-
message: "Free plans must have zero price"
|
|
961
|
-
});
|
|
1206
|
+
function isRouteStatusCodePolicyString(value) {
|
|
1207
|
+
return value.split(",").every(statusPolicyPartIsValid);
|
|
962
1208
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1209
|
+
var routeStatusCodePolicySchema = z17.union([
|
|
1210
|
+
z17.string().min(1).max(100).refine(isRouteStatusCodePolicyString, {
|
|
1211
|
+
message: "onStatusCodes must be comma-separated HTTP status codes or numeric ranges, e.g. 200-299,304"
|
|
1212
|
+
}),
|
|
1213
|
+
z17.array(z17.number().int().min(100).max(599)).min(1).max(100)
|
|
1214
|
+
]);
|
|
1215
|
+
var routeDefinitionSchema = z17.object({
|
|
1216
|
+
match: routeMatchSchema,
|
|
1217
|
+
metering: z17.object({
|
|
1218
|
+
defaults: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1219
|
+
reports: z17.array(z17.string().min(1).max(64)).max(20).optional(),
|
|
1220
|
+
estimates: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1221
|
+
onStatusCodes: routeStatusCodePolicySchema.optional()
|
|
1222
|
+
}).optional(),
|
|
1223
|
+
unmetered: z17.boolean().optional(),
|
|
1224
|
+
inheritDefaultMeters: z17.boolean().optional(),
|
|
1225
|
+
/** Optional explicit action id. When absent, the compiler derives an
|
|
1226
|
+
* implicit action from feature + method + path. */
|
|
1227
|
+
action: z17.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/).optional(),
|
|
1228
|
+
/** BYO-Backend V1 — optional route→backend binding. Omitted = the sole /
|
|
1229
|
+
* default backend (single-backend products stay zero-config). The schema
|
|
1230
|
+
* is `.strict()`, so this key MUST be declared here or `parse` throws on
|
|
1231
|
+
* it (anti-`.strict()`). */
|
|
1232
|
+
backend: routeBackendBindingSchema.optional()
|
|
1233
|
+
}).strict();
|
|
1234
|
+
var routeUpstreamSchema = z17.object({
|
|
1235
|
+
override_origin: z17.string().url("override_origin must be a valid URL").nullable().default(null)
|
|
1236
|
+
});
|
|
1237
|
+
var routeRuntimeSchema = z17.object({
|
|
1238
|
+
rollout_key: z17.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "rollout_key must be lowercase alphanumeric with hyphens/underscores").optional(),
|
|
1239
|
+
/**
|
|
1240
|
+
* Optional runtime flags this feature depends on. The runtime
|
|
1241
|
+
* evaluator AND's the feature's enablement across all referenced
|
|
1242
|
+
* flags. If any flag is disabled, the route returns the configured
|
|
1243
|
+
* fallback (404 by default — see /runtime failure matrix).
|
|
1244
|
+
*/
|
|
1245
|
+
required_flags: z17.array(z17.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(10).optional()
|
|
1246
|
+
});
|
|
1247
|
+
var routeLayerSchema = z17.object({
|
|
1248
|
+
/**
|
|
1249
|
+
* Feature key — the entitlement unit. Surfaced in dashboards,
|
|
1250
|
+
* subscriptions, and the gateway's matched-route trace.
|
|
1251
|
+
*/
|
|
1252
|
+
feature: z17.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/, "feature key must be lowercase alphanumeric with [_.:-]"),
|
|
1253
|
+
description: z17.string().max(500).optional(),
|
|
1254
|
+
/**
|
|
1255
|
+
* Route additions are contractual by default — they expose new API
|
|
1256
|
+
* surface to subscribers. Internal/non-customer-visible routes can
|
|
1257
|
+
* mark themselves `runtime` to allow autonomous agent flips
|
|
1258
|
+
* (invariant #16; see RFC approval matrix).
|
|
1259
|
+
*/
|
|
1260
|
+
mutation_class: z17.enum(["runtime", "contractual"]).default("contractual"),
|
|
1261
|
+
cacheProfile: cacheProfileSchema,
|
|
1262
|
+
routes: z17.array(routeDefinitionSchema).min(1).max(50),
|
|
1263
|
+
upstream: routeUpstreamSchema.default({ override_origin: null }),
|
|
1264
|
+
/**
|
|
1265
|
+
* Ordered list of policy names to apply. Executed sequentially by
|
|
1266
|
+
* the gateway policy engine; first-deny wins. Referenced policies
|
|
1267
|
+
* MUST declare compatible `compatible_with` envelopes for this
|
|
1268
|
+
* feature's route/meter shape — the compiler enforces.
|
|
1269
|
+
*/
|
|
1270
|
+
policies: z17.array(z17.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
|
|
1271
|
+
runtime: routeRuntimeSchema.default({}),
|
|
1272
|
+
/**
|
|
1273
|
+
* Plans that grant this feature directly. Shared feature bundles are
|
|
1274
|
+
* expressed in capability layers via `includes_features`; route layers do
|
|
1275
|
+
* not declare capability membership.
|
|
1276
|
+
*/
|
|
1277
|
+
plans: z17.array(z17.string().min(1).max(64)).max(20).default([]),
|
|
1278
|
+
/** Explicit actions declared by this feature. Routes reference them by
|
|
1279
|
+
* `route.action`; routes without a binding receive implicit actions. */
|
|
1280
|
+
actions: z17.array(actionSpecSchema).max(100).optional(),
|
|
1281
|
+
/** BYO-Backend V1 — feature-level default backend binding. Routes in this
|
|
1282
|
+
* layer with no explicit `route.backend` inherit this; routes may still
|
|
1283
|
+
* override per-route. Omitted = the product's sole / default backend. */
|
|
1284
|
+
backend: routeBackendBindingSchema.optional()
|
|
1285
|
+
}).strict();
|
|
1286
|
+
|
|
1287
|
+
// ../contracts/dist/plans/spec/refinements.js
|
|
1288
|
+
function rejectUsagePricing(spec, ctx) {
|
|
1289
|
+
if (spec.usagePricing === void 0)
|
|
1290
|
+
return;
|
|
1291
|
+
ctx.addIssue({
|
|
1292
|
+
code: "custom",
|
|
1293
|
+
path: ["usagePricing"],
|
|
1294
|
+
message: "usagePricing is not supported. Define usage.meters.<key>.rating instead."
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
function validateFreePlans(plans, ctx) {
|
|
1298
|
+
const freePlans = plans.filter((plan) => plan.free);
|
|
1299
|
+
if (freePlans.length > 1) {
|
|
1300
|
+
ctx.addIssue({
|
|
1301
|
+
code: "custom",
|
|
1302
|
+
path: ["plans"],
|
|
1303
|
+
message: "Only one free plan is allowed per product"
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
plans.forEach((plan, index) => {
|
|
1307
|
+
if (!plan.free)
|
|
1308
|
+
return;
|
|
1309
|
+
validateFreePlanPrice(plan, index, ctx);
|
|
1310
|
+
validateFreePlanHardLimit(plan, index, ctx);
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
function validateFreePlanPrice(plan, index, ctx) {
|
|
1314
|
+
const monthly = planMonthlyPrice(plan);
|
|
1315
|
+
if ((monthly ?? 0) === 0)
|
|
1316
|
+
return;
|
|
1317
|
+
ctx.addIssue({
|
|
1318
|
+
code: "custom",
|
|
1319
|
+
path: ["plans", index, "recurring_fee_cents"],
|
|
1320
|
+
message: "Free plans must have zero price"
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
function validateFreePlanHardLimit(plan, index, ctx) {
|
|
1324
|
+
const hasHardLimit = plan.limits.some((limit) => !limit.enforcement || limit.enforcement === "enforce");
|
|
1325
|
+
if (hasHardLimit)
|
|
1326
|
+
return;
|
|
967
1327
|
ctx.addIssue({
|
|
968
1328
|
code: "custom",
|
|
969
1329
|
path: ["plans", index, "limits"],
|
|
@@ -1166,6 +1526,68 @@ function validateLimitMeterReachability(spec, ctx) {
|
|
|
1166
1526
|
});
|
|
1167
1527
|
});
|
|
1168
1528
|
}
|
|
1529
|
+
function validateBackendReferences(spec, ctx) {
|
|
1530
|
+
const backends = spec.backend;
|
|
1531
|
+
if (!backends || Object.keys(backends).length === 0)
|
|
1532
|
+
return;
|
|
1533
|
+
const backendIds = new Set(Object.keys(backends));
|
|
1534
|
+
const productMeterKeys = new Set((spec.metering?.meters ?? []).map((m) => m.key).filter((k) => typeof k === "string" && k.length > 0));
|
|
1535
|
+
for (const [backendId, backend] of Object.entries(backends)) {
|
|
1536
|
+
(backend.meters ?? []).forEach((meter, meterIdx) => {
|
|
1537
|
+
if (productMeterKeys.has(meter))
|
|
1538
|
+
return;
|
|
1539
|
+
ctx.addIssue({
|
|
1540
|
+
code: "custom",
|
|
1541
|
+
path: ["backend", backendId, "meters", meterIdx],
|
|
1542
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.unknownMeterInBackend}: backend "${backendId}" allows meter "${meter}" but no metering.meters[] entry declares it.`
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
const { defaultId, ambiguous } = resolveDefaultBackendId(backends);
|
|
1547
|
+
let anyRouteOmitsBinding = false;
|
|
1548
|
+
for (const feature of Object.values(spec.features ?? {})) {
|
|
1549
|
+
for (const route of feature.routes ?? []) {
|
|
1550
|
+
if (route.backend === void 0)
|
|
1551
|
+
anyRouteOmitsBinding = true;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (ambiguous && anyRouteOmitsBinding) {
|
|
1555
|
+
ctx.addIssue({
|
|
1556
|
+
code: "custom",
|
|
1557
|
+
path: ["backend"],
|
|
1558
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.ambiguousDefaultBackend}: product declares ${backendIds.size} backends but no single default \u2014 mark exactly one backend \`default: true\` or bind every route explicitly.`
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
for (const [featureKey, feature] of Object.entries(spec.features ?? {})) {
|
|
1562
|
+
(feature.routes ?? []).forEach((route, routeIdx) => {
|
|
1563
|
+
const boundId = route.backend ?? defaultId;
|
|
1564
|
+
if (route.backend !== void 0 && !backendIds.has(route.backend)) {
|
|
1565
|
+
ctx.addIssue({
|
|
1566
|
+
code: "custom",
|
|
1567
|
+
path: ["features", featureKey, "routes", routeIdx, "backend"],
|
|
1568
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.unknownBackendInRoute}: route binds backend "${route.backend}" which is not declared in the product \`backend\` block.`
|
|
1569
|
+
});
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (!boundId)
|
|
1573
|
+
return;
|
|
1574
|
+
const backend = backends[boundId];
|
|
1575
|
+
const allow = backend?.meters;
|
|
1576
|
+
if (!allow)
|
|
1577
|
+
return;
|
|
1578
|
+
const allowed = new Set(allow);
|
|
1579
|
+
for (const meter of routeMeterKeys(route)) {
|
|
1580
|
+
if (allowed.has(meter))
|
|
1581
|
+
continue;
|
|
1582
|
+
ctx.addIssue({
|
|
1583
|
+
code: "custom",
|
|
1584
|
+
path: ["features", featureKey, "routes", routeIdx, "metering"],
|
|
1585
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.routeMeterNotAllowedByBackend}: route meters "${meter}" but backend "${boundId}" does not allow it (backend.meters[] = [${allow.join(", ")}]).`
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1169
1591
|
function planMonthlyPrice(plan) {
|
|
1170
1592
|
return plan.recurring_fee_cents;
|
|
1171
1593
|
}
|
|
@@ -1175,18 +1597,18 @@ function planPriceKey(plan, monthly) {
|
|
|
1175
1597
|
}
|
|
1176
1598
|
|
|
1177
1599
|
// ../contracts/dist/plans/spec/product.js
|
|
1178
|
-
var productIdentitySchema =
|
|
1179
|
-
subdomain:
|
|
1600
|
+
var productIdentitySchema = z18.object({
|
|
1601
|
+
subdomain: z18.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "Subdomain must be lowercase alphanumeric with optional hyphens")
|
|
1180
1602
|
});
|
|
1181
|
-
var meterEnforcementTypeSchema =
|
|
1603
|
+
var meterEnforcementTypeSchema = z18.enum([
|
|
1182
1604
|
"exact_pre_request",
|
|
1183
1605
|
"estimated_then_settled",
|
|
1184
1606
|
"postpaid",
|
|
1185
1607
|
"strict_concurrency"
|
|
1186
1608
|
]);
|
|
1187
|
-
var meterDefinitionSchema =
|
|
1188
|
-
key:
|
|
1189
|
-
display:
|
|
1609
|
+
var meterDefinitionSchema = z18.object({
|
|
1610
|
+
key: z18.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Meter key must be lowercase alphanumeric with underscores"),
|
|
1611
|
+
display: z18.string().min(1).max(100),
|
|
1190
1612
|
// v0.42.0 — `type: "built-in" | "custom"` removed. The runtime never
|
|
1191
1613
|
// read it (gateway estimators key on meter NAME, Stripe meter
|
|
1192
1614
|
// creation keys on meter KEY); it was schema documentation. Wizard
|
|
@@ -1194,11 +1616,11 @@ var meterDefinitionSchema = z13.object({
|
|
|
1194
1616
|
// ai_usage → `dollars`); Custom template lets builders define keys
|
|
1195
1617
|
// freely. Old specs with `type: ...` parse cleanly because Zod
|
|
1196
1618
|
// strips unknown fields by default.
|
|
1197
|
-
unit:
|
|
1619
|
+
unit: z18.string().max(20).optional(),
|
|
1198
1620
|
/** Reusable pre-request estimate for routes that dynamically report this meter. */
|
|
1199
|
-
estimate:
|
|
1621
|
+
estimate: z18.number().finite().nonnegative().optional(),
|
|
1200
1622
|
/** Fixed per-request default applied by Product SDK helpers. */
|
|
1201
|
-
routeDefault:
|
|
1623
|
+
routeDefault: z18.number().finite().nonnegative().optional(),
|
|
1202
1624
|
/**
|
|
1203
1625
|
* Runtime enforcement semantics for this meter. This is compiled into
|
|
1204
1626
|
* signed gateway artifacts so the edge chooses reservation, settlement,
|
|
@@ -1223,7 +1645,7 @@ var meterDefinitionSchema = z13.object({
|
|
|
1223
1645
|
* current `window`. Defaults to `COUNT` (one event = one unit) so
|
|
1224
1646
|
* existing meters that didn't declare aggregation continue to work.
|
|
1225
1647
|
*/
|
|
1226
|
-
aggregation:
|
|
1648
|
+
aggregation: z18.enum(["SUM", "COUNT", "MAX", "UNIQUE_COUNT", "LATEST"]).default("COUNT"),
|
|
1227
1649
|
/**
|
|
1228
1650
|
* Aggregation window. `billing_period` (the default) makes the
|
|
1229
1651
|
* meter accumulate across the subscription's billing period.
|
|
@@ -1231,91 +1653,88 @@ var meterDefinitionSchema = z13.object({
|
|
|
1231
1653
|
* rate-limit-shaped meters where the period boundary is a fixed
|
|
1232
1654
|
* wall-clock interval, not the subscription anniversary.
|
|
1233
1655
|
*/
|
|
1234
|
-
window:
|
|
1656
|
+
window: z18.enum(["minute", "hour", "day", "month", "billing_period"]).default("billing_period"),
|
|
1235
1657
|
/**
|
|
1236
1658
|
* Property on the event payload to read for `SUM` and `MAX`
|
|
1237
1659
|
* aggregations. Optional at the schema level; core's `validate.ts`
|
|
1238
1660
|
* (Phase 1b) enforces "required when aggregation is SUM or MAX".
|
|
1239
1661
|
*/
|
|
1240
|
-
valueProperty:
|
|
1662
|
+
valueProperty: z18.string().optional(),
|
|
1241
1663
|
/**
|
|
1242
1664
|
* Property on the event payload to dedupe on for `UNIQUE_COUNT`
|
|
1243
1665
|
* aggregation. Optional at the schema level; core's validate-pass
|
|
1244
1666
|
* enforces "required when aggregation is UNIQUE_COUNT".
|
|
1245
1667
|
*/
|
|
1246
|
-
uniqueProperty:
|
|
1668
|
+
uniqueProperty: z18.string().optional(),
|
|
1247
1669
|
/**
|
|
1248
1670
|
* Optional grouping dimensions. When set, the aggregation is per
|
|
1249
1671
|
* unique combination of these properties' values, not a single
|
|
1250
1672
|
* scalar. Used for per-region or per-model breakdowns.
|
|
1251
1673
|
*/
|
|
1252
|
-
groupBy:
|
|
1674
|
+
groupBy: z18.array(z18.string()).optional(),
|
|
1253
1675
|
/**
|
|
1254
1676
|
* Lago-side event code for ingress correlation. Matches Lago's
|
|
1255
1677
|
* BillableMetric `code` so events sent to Lago land on the right
|
|
1256
1678
|
* meter without a per-meter translation table.
|
|
1257
1679
|
*/
|
|
1258
|
-
eventCode:
|
|
1680
|
+
eventCode: z18.string().optional()
|
|
1259
1681
|
});
|
|
1260
|
-
var usageMeasureSchema =
|
|
1261
|
-
var usageRatingPricePolicySchema =
|
|
1682
|
+
var usageMeasureSchema = z18.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Usage measure must be lowercase alphanumeric with underscores");
|
|
1683
|
+
var usageRatingPricePolicySchema = z18.enum([
|
|
1262
1684
|
"pass_through",
|
|
1263
1685
|
"markup",
|
|
1264
1686
|
"fixed_margin",
|
|
1265
1687
|
"customer_rate"
|
|
1266
1688
|
]);
|
|
1267
|
-
var fixedRatingSchema =
|
|
1268
|
-
source:
|
|
1269
|
-
rates:
|
|
1689
|
+
var fixedRatingSchema = z18.object({
|
|
1690
|
+
source: z18.literal("fixed"),
|
|
1691
|
+
rates: z18.record(z18.string().min(1), z18.record(usageMeasureSchema, z18.number().int().nonnegative()))
|
|
1270
1692
|
});
|
|
1271
|
-
var providerCatalogRatingSchema =
|
|
1272
|
-
source:
|
|
1273
|
-
catalog:
|
|
1693
|
+
var providerCatalogRatingSchema = z18.object({
|
|
1694
|
+
source: z18.literal("provider_catalog"),
|
|
1695
|
+
catalog: z18.string().min(1).max(100),
|
|
1274
1696
|
pricePolicy: usageRatingPricePolicySchema.default("pass_through"),
|
|
1275
|
-
markupPercent:
|
|
1276
|
-
marginMicros:
|
|
1697
|
+
markupPercent: z18.number().nonnegative().max(1e4).optional(),
|
|
1698
|
+
marginMicros: z18.number().int().nonnegative().optional()
|
|
1277
1699
|
});
|
|
1278
|
-
var upstreamReportedRatingSchema =
|
|
1279
|
-
source:
|
|
1280
|
-
amountField:
|
|
1281
|
-
currencyField:
|
|
1700
|
+
var upstreamReportedRatingSchema = z18.object({
|
|
1701
|
+
source: z18.literal("upstream_reported"),
|
|
1702
|
+
amountField: z18.string().min(1).max(500),
|
|
1703
|
+
currencyField: z18.string().min(1).max(500).optional()
|
|
1282
1704
|
});
|
|
1283
|
-
var externalRateApiRatingSchema =
|
|
1284
|
-
source:
|
|
1285
|
-
resolver:
|
|
1286
|
-
configRef:
|
|
1705
|
+
var externalRateApiRatingSchema = z18.object({
|
|
1706
|
+
source: z18.literal("external_rate_api"),
|
|
1707
|
+
resolver: z18.string().min(1).max(100),
|
|
1708
|
+
configRef: z18.string().min(1).max(200).optional()
|
|
1287
1709
|
});
|
|
1288
|
-
var customRatingSchema =
|
|
1289
|
-
source:
|
|
1290
|
-
resolver:
|
|
1291
|
-
configRef:
|
|
1710
|
+
var customRatingSchema = z18.object({
|
|
1711
|
+
source: z18.literal("custom"),
|
|
1712
|
+
resolver: z18.string().min(1).max(100),
|
|
1713
|
+
configRef: z18.string().min(1).max(200).optional()
|
|
1292
1714
|
});
|
|
1293
|
-
var usageRatingSchema =
|
|
1715
|
+
var usageRatingSchema = z18.discriminatedUnion("source", [
|
|
1294
1716
|
fixedRatingSchema,
|
|
1295
1717
|
providerCatalogRatingSchema,
|
|
1296
1718
|
upstreamReportedRatingSchema,
|
|
1297
1719
|
externalRateApiRatingSchema,
|
|
1298
1720
|
customRatingSchema
|
|
1299
1721
|
]);
|
|
1300
|
-
var usageMeterSchema =
|
|
1301
|
-
selector:
|
|
1302
|
-
measures:
|
|
1722
|
+
var usageMeterSchema = z18.object({
|
|
1723
|
+
selector: z18.string().min(1).max(100).optional(),
|
|
1724
|
+
measures: z18.array(usageMeasureSchema).min(1).max(20),
|
|
1303
1725
|
rating: usageRatingSchema.optional()
|
|
1304
1726
|
});
|
|
1305
|
-
var usageBlockSchema =
|
|
1306
|
-
meters:
|
|
1727
|
+
var usageBlockSchema = z18.object({
|
|
1728
|
+
meters: z18.record(z18.string().min(1).max(64).regex(/^[a-z0-9_]+$/), usageMeterSchema)
|
|
1307
1729
|
});
|
|
1308
|
-
var routeMeteringSchema =
|
|
1309
|
-
defaults:
|
|
1310
|
-
reports:
|
|
1311
|
-
estimates:
|
|
1312
|
-
onStatusCodes:
|
|
1313
|
-
z13.string().min(1).max(100),
|
|
1314
|
-
z13.array(z13.number().int().min(100).max(599)).min(1).max(100)
|
|
1315
|
-
]).optional()
|
|
1730
|
+
var routeMeteringSchema = z18.object({
|
|
1731
|
+
defaults: z18.record(z18.string().min(1).max(64), z18.number().finite().nonnegative()).optional(),
|
|
1732
|
+
reports: z18.array(z18.string().min(1).max(64)).max(20).optional(),
|
|
1733
|
+
estimates: z18.record(z18.string().min(1).max(64), z18.number().finite().nonnegative()).optional(),
|
|
1734
|
+
onStatusCodes: routeStatusCodePolicySchema.optional()
|
|
1316
1735
|
});
|
|
1317
|
-
var featureRouteSchema =
|
|
1318
|
-
method:
|
|
1736
|
+
var featureRouteSchema = z18.object({
|
|
1737
|
+
method: z18.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
|
|
1319
1738
|
// Path is the route under the product's baseUrl. OpenAPI parameter
|
|
1320
1739
|
// syntax is supported and translated by the compiler:
|
|
1321
1740
|
// /users/:id → /users/*
|
|
@@ -1323,44 +1742,48 @@ var featureRouteSchema = z13.object({
|
|
|
1323
1742
|
// Path-globs `*` (one segment) and `**` (any subpath) are passed
|
|
1324
1743
|
// through. The compiler rejects ambiguous compound segments like
|
|
1325
1744
|
// `/foo/:a-:b` — parameter names must occupy whole segments.
|
|
1326
|
-
path:
|
|
1745
|
+
path: z18.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]"),
|
|
1327
1746
|
// Explicit no-usage route. Dynamic/static route metering is declared
|
|
1328
1747
|
// exclusively under `metering`.
|
|
1329
|
-
unmetered:
|
|
1748
|
+
unmetered: z18.boolean().optional(),
|
|
1330
1749
|
metering: routeMeteringSchema.optional(),
|
|
1331
|
-
inheritDefaultMeters:
|
|
1750
|
+
inheritDefaultMeters: z18.boolean().optional(),
|
|
1751
|
+
// BYO-Backend V1 — route→backend binding. The compiler materializes
|
|
1752
|
+
// per-feature route layers into this strict route shape, so the key MUST be
|
|
1753
|
+
// declared here or `parse` throws on it (anti-`.strict()`).
|
|
1754
|
+
backend: routeBackendBindingSchema.optional()
|
|
1332
1755
|
}).strict();
|
|
1333
|
-
var featureCatalogEntrySchema =
|
|
1756
|
+
var featureCatalogEntrySchema = z18.object({
|
|
1334
1757
|
// Optional human-friendly summary; surfaced in dashboards / settings UI.
|
|
1335
|
-
description:
|
|
1336
|
-
routes:
|
|
1758
|
+
description: z18.string().max(500).optional(),
|
|
1759
|
+
routes: z18.array(featureRouteSchema).min(1).max(50),
|
|
1337
1760
|
// Plans that grant this feature. Feature-first canonical mapping —
|
|
1338
1761
|
// builders declare "which plans get this feature" on the feature
|
|
1339
1762
|
// itself rather than enumerating features per plan. Required and
|
|
1340
1763
|
// non-empty: a feature with no plans grants nothing and is a likely
|
|
1341
1764
|
// typo. Cross-reference validation (every key resolves to an
|
|
1342
1765
|
// existing plan) lives in `validateFeatureReferences` below.
|
|
1343
|
-
plans:
|
|
1766
|
+
plans: z18.array(z18.string().min(1)).min(1).max(20)
|
|
1344
1767
|
});
|
|
1345
|
-
var featureCatalogSchema =
|
|
1346
|
-
var productCleanupPolicyModeSchema =
|
|
1768
|
+
var featureCatalogSchema = z18.record(z18.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
|
|
1769
|
+
var productCleanupPolicyModeSchema = z18.enum([
|
|
1347
1770
|
"report",
|
|
1348
1771
|
"pull_request"
|
|
1349
1772
|
]);
|
|
1350
|
-
var productChangeApprovalRiskSchema =
|
|
1773
|
+
var productChangeApprovalRiskSchema = z18.enum([
|
|
1351
1774
|
"safe",
|
|
1352
1775
|
"non_blocking",
|
|
1353
1776
|
"economic_risk",
|
|
1354
1777
|
"blocking"
|
|
1355
1778
|
]);
|
|
1356
|
-
var productOperatorPoliciesSchema =
|
|
1779
|
+
var productOperatorPoliciesSchema = z18.object({
|
|
1357
1780
|
/**
|
|
1358
1781
|
* Route cleanup operator. Disabled by default; report-mode is the safe
|
|
1359
1782
|
* default so a product can surface zero-traffic runtime-route candidates
|
|
1360
1783
|
* before it opts into draft PR mutation.
|
|
1361
1784
|
*/
|
|
1362
|
-
cleanup:
|
|
1363
|
-
enabled:
|
|
1785
|
+
cleanup: z18.object({
|
|
1786
|
+
enabled: z18.boolean().default(false),
|
|
1364
1787
|
mode: productCleanupPolicyModeSchema.default("report")
|
|
1365
1788
|
}).default({ enabled: false, mode: "report" }),
|
|
1366
1789
|
/**
|
|
@@ -1368,9 +1791,9 @@ var productOperatorPoliciesSchema = z13.object({
|
|
|
1368
1791
|
* the policy a first-class product-as-code field even while enforcement is
|
|
1369
1792
|
* still report/label-only.
|
|
1370
1793
|
*/
|
|
1371
|
-
change_approval:
|
|
1372
|
-
auto_merge_max_risk:
|
|
1373
|
-
require_human_for:
|
|
1794
|
+
change_approval: z18.object({
|
|
1795
|
+
auto_merge_max_risk: z18.enum(["none", "safe", "non_blocking"]).default("none"),
|
|
1796
|
+
require_human_for: z18.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
|
|
1374
1797
|
}).default({
|
|
1375
1798
|
auto_merge_max_risk: "none",
|
|
1376
1799
|
require_human_for: ["economic_risk", "blocking"]
|
|
@@ -1382,15 +1805,15 @@ var productOperatorPoliciesSchema = z13.object({
|
|
|
1382
1805
|
require_human_for: ["economic_risk", "blocking"]
|
|
1383
1806
|
}
|
|
1384
1807
|
});
|
|
1385
|
-
var customerIdentityRequirementSchema =
|
|
1808
|
+
var customerIdentityRequirementSchema = z18.enum([
|
|
1386
1809
|
"org_only",
|
|
1387
1810
|
"org_and_user"
|
|
1388
1811
|
]);
|
|
1389
|
-
var customerPortalAuthStrategySchema =
|
|
1812
|
+
var customerPortalAuthStrategySchema = z18.enum([
|
|
1390
1813
|
"clerk",
|
|
1391
1814
|
"test-personas"
|
|
1392
1815
|
]);
|
|
1393
|
-
var productCustomerContextSchema =
|
|
1816
|
+
var productCustomerContextSchema = z18.object({
|
|
1394
1817
|
/**
|
|
1395
1818
|
* Edge credential identity policy. This is intentionally Product-scoped:
|
|
1396
1819
|
* B7 keeps Product as the product boundary and avoids customer-side
|
|
@@ -1402,19 +1825,19 @@ var productCustomerContextSchema = z13.object({
|
|
|
1402
1825
|
* runtime signing secret in product/product.config.ts. Core generates/preserves the
|
|
1403
1826
|
* secret when this is true and clears it when explicitly false.
|
|
1404
1827
|
*/
|
|
1405
|
-
context_tokens:
|
|
1406
|
-
enabled:
|
|
1828
|
+
context_tokens: z18.object({
|
|
1829
|
+
enabled: z18.boolean().default(true)
|
|
1407
1830
|
}).optional(),
|
|
1408
1831
|
/**
|
|
1409
1832
|
* Portal auth strategy for environment-scoped product applies. Production
|
|
1410
1833
|
* portal auth is provisioner-owned; preview/test environments can opt into
|
|
1411
1834
|
* test personas through Product-as-Code.
|
|
1412
1835
|
*/
|
|
1413
|
-
portal_auth:
|
|
1836
|
+
portal_auth: z18.object({
|
|
1414
1837
|
strategy: customerPortalAuthStrategySchema
|
|
1415
1838
|
}).optional()
|
|
1416
1839
|
});
|
|
1417
|
-
var productSurfaceTypeSchema =
|
|
1840
|
+
var productSurfaceTypeSchema = z18.enum([
|
|
1418
1841
|
"frontend",
|
|
1419
1842
|
"api",
|
|
1420
1843
|
"docs",
|
|
@@ -1424,87 +1847,99 @@ var productSurfaceTypeSchema = z13.enum([
|
|
|
1424
1847
|
"worker",
|
|
1425
1848
|
"agent"
|
|
1426
1849
|
]);
|
|
1427
|
-
var productSurfaceSchema =
|
|
1428
|
-
key:
|
|
1850
|
+
var productSurfaceSchema = z18.object({
|
|
1851
|
+
key: z18.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Surface key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1429
1852
|
type: productSurfaceTypeSchema,
|
|
1430
|
-
display:
|
|
1431
|
-
description:
|
|
1432
|
-
});
|
|
1433
|
-
var productEntitlementSchema =
|
|
1434
|
-
key:
|
|
1435
|
-
description:
|
|
1436
|
-
capabilities:
|
|
1437
|
-
featureGates:
|
|
1438
|
-
limits:
|
|
1439
|
-
meters:
|
|
1440
|
-
});
|
|
1441
|
-
var productSurfacesSchema =
|
|
1442
|
-
var productEntitlementsSchema =
|
|
1443
|
-
var productWorkflowKindSchema =
|
|
1853
|
+
display: z18.string().min(1).max(100).optional(),
|
|
1854
|
+
description: z18.string().max(500).optional()
|
|
1855
|
+
});
|
|
1856
|
+
var productEntitlementSchema = z18.object({
|
|
1857
|
+
key: z18.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Entitlement key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1858
|
+
description: z18.string().max(500).optional(),
|
|
1859
|
+
capabilities: z18.array(z18.string().min(1).max(100)).max(100).optional(),
|
|
1860
|
+
featureGates: z18.record(z18.string().min(1), z18.boolean()).optional(),
|
|
1861
|
+
limits: z18.array(planLimitRuleSchema).max(100).optional(),
|
|
1862
|
+
meters: z18.array(z18.string().min(1).max(64)).max(100).optional()
|
|
1863
|
+
});
|
|
1864
|
+
var productSurfacesSchema = z18.array(productSurfaceSchema).max(20).default([]);
|
|
1865
|
+
var productEntitlementsSchema = z18.array(productEntitlementSchema).max(100).default([]);
|
|
1866
|
+
var productWorkflowKindSchema = z18.enum([
|
|
1444
1867
|
"async_job",
|
|
1445
1868
|
"agent_task",
|
|
1446
1869
|
"scheduled",
|
|
1447
1870
|
"lifecycle",
|
|
1448
1871
|
"background"
|
|
1449
1872
|
]);
|
|
1450
|
-
var productWorkflowTriggerSchema =
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
type:
|
|
1454
|
-
cron:
|
|
1873
|
+
var productWorkflowTriggerSchema = z18.discriminatedUnion("type", [
|
|
1874
|
+
z18.object({ type: z18.literal("manual") }),
|
|
1875
|
+
z18.object({
|
|
1876
|
+
type: z18.literal("schedule"),
|
|
1877
|
+
cron: z18.string().min(1).max(120)
|
|
1455
1878
|
}),
|
|
1456
|
-
|
|
1457
|
-
type:
|
|
1458
|
-
event:
|
|
1879
|
+
z18.object({
|
|
1880
|
+
type: z18.literal("event"),
|
|
1881
|
+
event: z18.string().min(1).max(120)
|
|
1459
1882
|
}),
|
|
1460
|
-
|
|
1461
|
-
type:
|
|
1462
|
-
path:
|
|
1883
|
+
z18.object({
|
|
1884
|
+
type: z18.literal("api"),
|
|
1885
|
+
path: z18.string().min(1).max(240).regex(/^\//, "path must start with /")
|
|
1463
1886
|
}),
|
|
1464
|
-
|
|
1465
|
-
type:
|
|
1466
|
-
event:
|
|
1887
|
+
z18.object({
|
|
1888
|
+
type: z18.literal("lifecycle"),
|
|
1889
|
+
event: z18.string().min(1).max(120)
|
|
1467
1890
|
})
|
|
1468
1891
|
]);
|
|
1469
|
-
var productWorkflowSchema =
|
|
1470
|
-
key:
|
|
1471
|
-
title:
|
|
1472
|
-
description:
|
|
1892
|
+
var productWorkflowSchema = z18.object({
|
|
1893
|
+
key: z18.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Workflow key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1894
|
+
title: z18.string().min(1).max(120).optional(),
|
|
1895
|
+
description: z18.string().max(1e3).optional(),
|
|
1473
1896
|
kind: productWorkflowKindSchema,
|
|
1474
1897
|
trigger: productWorkflowTriggerSchema,
|
|
1475
|
-
capabilities:
|
|
1476
|
-
meters:
|
|
1477
|
-
estimates:
|
|
1478
|
-
metadata:
|
|
1479
|
-
});
|
|
1480
|
-
var productWorkflowsSchema =
|
|
1481
|
-
var productSpecSchema =
|
|
1482
|
-
product:
|
|
1483
|
-
name:
|
|
1484
|
-
displayName:
|
|
1485
|
-
description:
|
|
1486
|
-
baseUrl:
|
|
1487
|
-
sandboxBaseUrl:
|
|
1488
|
-
visibility:
|
|
1898
|
+
capabilities: z18.array(z18.string().min(1).max(100)).max(100).optional(),
|
|
1899
|
+
meters: z18.array(z18.string().min(1).max(64)).max(100).optional(),
|
|
1900
|
+
estimates: z18.record(z18.string().min(1).max(64), z18.number().finite()).optional(),
|
|
1901
|
+
metadata: z18.record(z18.string().min(1), z18.unknown()).optional()
|
|
1902
|
+
});
|
|
1903
|
+
var productWorkflowsSchema = z18.array(productWorkflowSchema).max(100).default([]);
|
|
1904
|
+
var productSpecSchema = z18.object({
|
|
1905
|
+
product: z18.object({
|
|
1906
|
+
name: z18.string().min(1).max(100),
|
|
1907
|
+
displayName: z18.string().max(200).optional(),
|
|
1908
|
+
description: z18.string().max(2e3).optional(),
|
|
1909
|
+
baseUrl: z18.string().url("baseUrl must be a valid URL"),
|
|
1910
|
+
sandboxBaseUrl: z18.string().url("sandboxBaseUrl must be a valid URL").optional(),
|
|
1911
|
+
visibility: z18.enum(["public", "private"]).default("public"),
|
|
1489
1912
|
// Branding
|
|
1490
|
-
logoUrl:
|
|
1491
|
-
primaryColor:
|
|
1913
|
+
logoUrl: z18.string().url().optional(),
|
|
1914
|
+
primaryColor: z18.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
1492
1915
|
// Environment
|
|
1493
|
-
envBranchPrefix:
|
|
1916
|
+
envBranchPrefix: z18.string().max(50).nullable().optional()
|
|
1494
1917
|
}),
|
|
1495
|
-
gateway:
|
|
1496
|
-
authHeader:
|
|
1497
|
-
upstreamAuth:
|
|
1498
|
-
type:
|
|
1499
|
-
token:
|
|
1918
|
+
gateway: z18.object({
|
|
1919
|
+
authHeader: z18.string().min(1).max(100).default("x-api-key"),
|
|
1920
|
+
upstreamAuth: z18.object({
|
|
1921
|
+
type: z18.enum(["none", "static_bearer"]),
|
|
1922
|
+
token: z18.string().optional()
|
|
1500
1923
|
}).default({ type: "none" })
|
|
1501
1924
|
}),
|
|
1502
|
-
metering:
|
|
1503
|
-
meters:
|
|
1504
|
-
billOn4xx:
|
|
1925
|
+
metering: z18.object({
|
|
1926
|
+
meters: z18.array(meterDefinitionSchema).max(10).default([]),
|
|
1927
|
+
billOn4xx: z18.boolean().default(false)
|
|
1505
1928
|
}).default({ meters: [], billOn4xx: false }),
|
|
1929
|
+
/**
|
|
1930
|
+
* BYO-Backend V1 — first-class backend declarations, keyed by backend id.
|
|
1931
|
+
* A product may declare MULTIPLE backends; routes bind to one (a default
|
|
1932
|
+
* applies when a product has exactly one backend OR exactly one is marked
|
|
1933
|
+
* `default: true`). Single-backend products stay zero-config.
|
|
1934
|
+
*
|
|
1935
|
+
* OPTIONAL and emits NO key when absent — a product with no `backend`
|
|
1936
|
+
* block hashes byte-identically to the pre-BYOB world. Cross-reference
|
|
1937
|
+
* validation (route→backend resolution, route-meter ∈ backend `meters[]`,
|
|
1938
|
+
* unambiguous default) lives in `validateBackendReferences`.
|
|
1939
|
+
*/
|
|
1940
|
+
backend: productBackendBlockSchema.optional(),
|
|
1506
1941
|
usage: usageBlockSchema.optional(),
|
|
1507
|
-
usagePricing:
|
|
1942
|
+
usagePricing: z18.never({
|
|
1508
1943
|
error: "usagePricing is not supported. Define usage.meters.<key>.rating instead."
|
|
1509
1944
|
}).optional(),
|
|
1510
1945
|
features: featureCatalogSchema.optional(),
|
|
@@ -1528,12 +1963,12 @@ var productSpecSchema = z13.object({
|
|
|
1528
1963
|
// `max_monthly_spend_cents`). The product-level `billing` block
|
|
1529
1964
|
// retains the transition-policy fields (`gracePeriodDays`,
|
|
1530
1965
|
// `subscriberChangePolicy`); the strategy enum is gone.
|
|
1531
|
-
billing:
|
|
1532
|
-
gracePeriodDays:
|
|
1966
|
+
billing: z18.object({
|
|
1967
|
+
gracePeriodDays: z18.number().int().nonnegative().default(3),
|
|
1533
1968
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
1534
1969
|
// subscribers immediately; a DECREASE always defers to period end.
|
|
1535
1970
|
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
1536
|
-
applyLimitUpgradesInstantly:
|
|
1971
|
+
applyLimitUpgradesInstantly: z18.boolean().optional(),
|
|
1537
1972
|
subscriberChangePolicy: subscriberChangePolicySchema.default({
|
|
1538
1973
|
default: "preserve_current_period",
|
|
1539
1974
|
proration: "none",
|
|
@@ -1571,7 +2006,7 @@ var productSpecSchema = z13.object({
|
|
|
1571
2006
|
allowImmediateEntitlementReduction: false
|
|
1572
2007
|
}
|
|
1573
2008
|
}),
|
|
1574
|
-
plans:
|
|
2009
|
+
plans: z18.array(planSpecSchema).max(4).default([]),
|
|
1575
2010
|
/**
|
|
1576
2011
|
* Add-on catalog (v0.56+). Composable economic + entitlement
|
|
1577
2012
|
* overlays that subscribers can pile on top of their base plan.
|
|
@@ -1614,16 +2049,16 @@ var productSpecSchema = z13.object({
|
|
|
1614
2049
|
* require_deprecation_window_days: 90
|
|
1615
2050
|
* require_successor_route: true
|
|
1616
2051
|
*/
|
|
1617
|
-
lifecycle:
|
|
1618
|
-
breaking_changes:
|
|
2052
|
+
lifecycle: z18.object({
|
|
2053
|
+
breaking_changes: z18.object({
|
|
1619
2054
|
/** Minimum days a route must have been marked for removal
|
|
1620
2055
|
* (in main-branch YAML) before the publish gate will let
|
|
1621
2056
|
* it actually be removed. Set to 0 to disable. */
|
|
1622
|
-
require_deprecation_window_days:
|
|
2057
|
+
require_deprecation_window_days: z18.number().int().nonnegative().default(0),
|
|
1623
2058
|
/** When true, a route removal must declare a successor
|
|
1624
2059
|
* route via the lifecycle metadata (mechanics in core
|
|
1625
2060
|
* 3b-2) before the publish gate accepts it. */
|
|
1626
|
-
require_successor_route:
|
|
2061
|
+
require_successor_route: z18.boolean().default(false)
|
|
1627
2062
|
}).default({
|
|
1628
2063
|
require_deprecation_window_days: 0,
|
|
1629
2064
|
require_successor_route: false
|
|
@@ -1669,8 +2104,8 @@ var productSpecSchema = z13.object({
|
|
|
1669
2104
|
* (preserves today's behaviour). Compiler validation pins the
|
|
1670
2105
|
* value to a real `plans[].key`.
|
|
1671
2106
|
*/
|
|
1672
|
-
ephemeral:
|
|
1673
|
-
defaultPlan:
|
|
2107
|
+
ephemeral: z18.object({
|
|
2108
|
+
defaultPlan: z18.string().min(1).optional()
|
|
1674
2109
|
}).optional()
|
|
1675
2110
|
}).superRefine((spec, ctx) => {
|
|
1676
2111
|
rejectUsagePricing(spec, ctx);
|
|
@@ -1680,813 +2115,1045 @@ var productSpecSchema = z13.object({
|
|
|
1680
2115
|
validateFeatureReferences(spec, ctx);
|
|
1681
2116
|
validateRouteMeters(spec, ctx);
|
|
1682
2117
|
validateLimitMeterReachability(spec, ctx);
|
|
2118
|
+
validateBackendReferences(spec, ctx);
|
|
1683
2119
|
});
|
|
1684
|
-
var productPhaseSchema =
|
|
2120
|
+
var productPhaseSchema = z18.object({
|
|
1685
2121
|
product: productSpecSchema.shape.product
|
|
1686
2122
|
});
|
|
1687
|
-
var gatewayPhaseSchema =
|
|
2123
|
+
var gatewayPhaseSchema = z18.object({
|
|
1688
2124
|
gateway: productSpecSchema.shape.gateway
|
|
1689
2125
|
});
|
|
1690
|
-
var meteringPhaseSchema =
|
|
2126
|
+
var meteringPhaseSchema = z18.object({
|
|
1691
2127
|
metering: productSpecSchema.shape.metering
|
|
1692
2128
|
});
|
|
1693
|
-
var plansPhaseSchema =
|
|
2129
|
+
var plansPhaseSchema = z18.object({
|
|
1694
2130
|
plans: productSpecSchema.shape.plans
|
|
1695
2131
|
});
|
|
1696
2132
|
|
|
1697
|
-
// ../contracts/dist/plans/spec/
|
|
1698
|
-
import { z as
|
|
1699
|
-
var
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
/**
|
|
1703
|
-
* Which request dimensions identify the bucket. v0.3.0 supports a
|
|
1704
|
-
* fixed set; extending requires a coordinated gateway/policy-engine
|
|
1705
|
-
* change. The `subscription` dimension is the steady-state default;
|
|
1706
|
-
* `ip` is for unauthenticated probes; `credential` is finer-grained
|
|
1707
|
-
* than subscription (per-key throttling).
|
|
1708
|
-
*/
|
|
1709
|
-
dimensions: z14.array(z14.enum(["subscription", "credential", "ip", "route"])).min(1).max(4).default(["subscription"]),
|
|
1710
|
-
limits: z14.array(z14.object({
|
|
1711
|
-
window: rateLimitWindowSchema,
|
|
1712
|
-
max: z14.number().int().positive().max(1e7)
|
|
1713
|
-
})).min(1).max(10),
|
|
1714
|
-
/**
|
|
1715
|
-
* Bounded fail-open behaviour for DO outages. See architecture RFC
|
|
1716
|
-
* "Fail-open guardrails" section. When the policy executor observes
|
|
1717
|
-
* `max_consecutive_failures` DO-call failures within
|
|
1718
|
-
* `max_window_seconds`, it transitions to `degraded_mode` until
|
|
1719
|
-
* `recovery_threshold` consecutive successes restore normal
|
|
1720
|
-
* evaluation.
|
|
1721
|
-
*/
|
|
1722
|
-
fail_open: z14.object({
|
|
1723
|
-
max_consecutive_failures: z14.number().int().positive().default(100),
|
|
1724
|
-
max_window_seconds: z14.number().int().positive().default(60),
|
|
1725
|
-
recovery_threshold: z14.number().int().positive().default(50),
|
|
1726
|
-
degraded_mode: z14.enum([
|
|
1727
|
-
"safe_mode_block",
|
|
1728
|
-
"safe_mode_throttle",
|
|
1729
|
-
"runtime_killswitch_trigger"
|
|
1730
|
-
]).default("safe_mode_throttle")
|
|
1731
|
-
}).default({
|
|
1732
|
-
max_consecutive_failures: 100,
|
|
1733
|
-
max_window_seconds: 60,
|
|
1734
|
-
recovery_threshold: 50,
|
|
1735
|
-
degraded_mode: "safe_mode_throttle"
|
|
1736
|
-
})
|
|
1737
|
-
});
|
|
1738
|
-
var authConfigSchema = z14.object({
|
|
1739
|
-
header_name: z14.string().min(1).max(100).default("x-api-key"),
|
|
1740
|
-
/**
|
|
1741
|
-
* How the gateway constructs the upstream Authorization header:
|
|
1742
|
-
* - `none` → no upstream auth header added
|
|
1743
|
-
* - `static_bearer` → forward a configured static token
|
|
1744
|
-
* - `subscriber_jwt` → mint a per-subscriber JWT (out of scope v0.3.0)
|
|
1745
|
-
*/
|
|
1746
|
-
upstream_token_source: z14.discriminatedUnion("type", [
|
|
1747
|
-
z14.object({ type: z14.literal("none") }),
|
|
1748
|
-
z14.object({
|
|
1749
|
-
type: z14.literal("static_bearer"),
|
|
1750
|
-
token_secret_ref: z14.string().min(1).max(200).describe("Reference into the secret store (e.g. CF Secret name); not the raw token")
|
|
1751
|
-
})
|
|
1752
|
-
]).default({ type: "none" }),
|
|
1753
|
-
/**
|
|
1754
|
-
* When `true`, treat the inbound credential's scopes (if any) as
|
|
1755
|
-
* additional gating beyond the entitlement check. v0.3.0 ships with
|
|
1756
|
-
* `strict` as the default.
|
|
1757
|
-
*/
|
|
1758
|
-
scope_mode: z14.enum(["strict", "advisory", "off"]).default("strict")
|
|
1759
|
-
});
|
|
1760
|
-
var concurrencyConfigSchema = z14.object({
|
|
1761
|
-
max_in_flight: z14.number().int().positive().max(1e4),
|
|
1762
|
-
/**
|
|
1763
|
-
* Which dimensions key the lease bucket. Matches the existing
|
|
1764
|
-
* ConcurrencyLease DO `idFromName` pattern (subscription | capability
|
|
1765
|
-
* tuple).
|
|
1766
|
-
*/
|
|
1767
|
-
dimensions: z14.array(z14.enum(["subscription", "credential", "capability"])).min(1).max(3).default(["subscription"]),
|
|
1768
|
-
/**
|
|
1769
|
-
* Optional capability scope. When set, the lease bucket is keyed
|
|
1770
|
-
* partly by this capability name — separate buckets per capability.
|
|
1771
|
-
*/
|
|
1772
|
-
capability: z14.string().min(1).max(120).optional(),
|
|
2133
|
+
// ../contracts/dist/plans/spec/capabilities-layer.js
|
|
2134
|
+
import { z as z19 } from "zod";
|
|
2135
|
+
var capabilityLayerSchema = z19.object({
|
|
2136
|
+
capability: z19.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "capability name must be lowercase alphanumeric with hyphens/underscores"),
|
|
2137
|
+
description: z19.string().max(500).optional(),
|
|
1773
2138
|
/**
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
2139
|
+
* Capability composition is contractual by default — including a new
|
|
2140
|
+
* feature changes the customer's effective entitlement. Mark
|
|
2141
|
+
* `runtime` only for capabilities that compose runtime-only knobs
|
|
2142
|
+
* (e.g. an internal "monitoring" capability that gates dashboard
|
|
2143
|
+
* pages without affecting billable behaviour).
|
|
1777
2144
|
*/
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
recovery_threshold: z14.number().int().positive().default(20),
|
|
1783
|
-
degraded_mode: z14.enum([
|
|
1784
|
-
"safe_mode_block",
|
|
1785
|
-
"safe_mode_throttle",
|
|
1786
|
-
"runtime_killswitch_trigger"
|
|
1787
|
-
]).default("safe_mode_throttle")
|
|
1788
|
-
}).default({
|
|
1789
|
-
max_consecutive_failures: 50,
|
|
1790
|
-
max_window_seconds: 60,
|
|
1791
|
-
recovery_threshold: 20,
|
|
1792
|
-
degraded_mode: "safe_mode_throttle"
|
|
1793
|
-
})
|
|
2145
|
+
mutation_class: z19.enum(["runtime", "contractual"]).default("contractual"),
|
|
2146
|
+
includes_features: z19.array(z19.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/)).max(20).default([]),
|
|
2147
|
+
includes_policies: z19.array(z19.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
|
|
2148
|
+
includes_capabilities: z19.array(z19.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(20).default([])
|
|
1794
2149
|
});
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
/**
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2150
|
+
|
|
2151
|
+
// ../contracts/dist/plans/spec/manifest-ir.js
|
|
2152
|
+
import { createHash } from "node:crypto";
|
|
2153
|
+
import { z as z20 } from "zod";
|
|
2154
|
+
var MANIFEST_IR_VERSION = 1;
|
|
2155
|
+
var manifestIrSchema = z20.object({
|
|
2156
|
+
irVersion: z20.literal(MANIFEST_IR_VERSION),
|
|
2157
|
+
/** Version of @farthershore/product that emitted this envelope. */
|
|
2158
|
+
sdkVersion: z20.string().min(1).max(64),
|
|
2159
|
+
/** Legacy unified ProductSpec — the live `CompileProductOptions.sourceSpec`. */
|
|
2160
|
+
product: productSpecSchema,
|
|
2161
|
+
/** One entry per feature, sorted by `feature`. */
|
|
2162
|
+
routes: z20.array(routeLayerSchema).max(200).default([]),
|
|
2163
|
+
/** Sorted by `name`. */
|
|
2164
|
+
policies: z20.array(policyLayerSchema).max(200).default([]),
|
|
2165
|
+
/** Sorted by `capability`. */
|
|
2166
|
+
capabilities: z20.array(capabilityLayerSchema).max(200).default([])
|
|
2167
|
+
}).strict();
|
|
2168
|
+
function canonicalManifestJson(value) {
|
|
2169
|
+
return stableJson(JSON.parse(JSON.stringify(value)));
|
|
2170
|
+
}
|
|
2171
|
+
function hashManifestIr(ir) {
|
|
2172
|
+
return createHash("sha256").update(canonicalManifestJson(ir)).digest("hex");
|
|
2173
|
+
}
|
|
2174
|
+
function stableJson(value) {
|
|
2175
|
+
if (Array.isArray(value))
|
|
2176
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
2177
|
+
if (value && typeof value === "object") {
|
|
2178
|
+
return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`).join(",")}}`;
|
|
2179
|
+
}
|
|
2180
|
+
return JSON.stringify(value);
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// ../contracts/dist/plans/presets.js
|
|
2184
|
+
var FREE = {
|
|
2185
|
+
kind: "free",
|
|
2186
|
+
label: "Free",
|
|
2187
|
+
description: "No recurring fee and no metered usage. Pure freemium tier.",
|
|
2188
|
+
pricing: {
|
|
2189
|
+
meters: [],
|
|
2190
|
+
recurring_fee_cents: 0,
|
|
2191
|
+
billing_interval: "month",
|
|
2192
|
+
grants: [],
|
|
2193
|
+
trial_days: 0
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
var STARTER = {
|
|
2197
|
+
kind: "starter",
|
|
2198
|
+
label: "Starter \u2014 $20/mo + $20 included",
|
|
2199
|
+
description: "$20/month subscription with $20 of usage credit each period. Stripe-style minimum-spend; overage billed at $0.001/request by default.",
|
|
2200
|
+
pricing: {
|
|
2201
|
+
meters: [
|
|
2202
|
+
{ dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
|
|
2203
|
+
],
|
|
2204
|
+
recurring_fee_cents: 2e3,
|
|
2205
|
+
billing_interval: "month",
|
|
2206
|
+
grants: [{ kind: "recurring", amount_cents: 2e3 }],
|
|
2207
|
+
trial_days: 0
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
var PRO = {
|
|
2211
|
+
kind: "pro",
|
|
2212
|
+
label: "Pro \u2014 $100/mo + $200 included",
|
|
2213
|
+
description: "$100/month subscription with $200 of usage credit each period and a 14-day trial. Overage billed at $0.0005/request by default.",
|
|
2214
|
+
pricing: {
|
|
2215
|
+
meters: [
|
|
2216
|
+
{ dimension: "requests", kind: "linear", price_per_unit_micros: 500 }
|
|
2217
|
+
],
|
|
2218
|
+
recurring_fee_cents: 1e4,
|
|
2219
|
+
billing_interval: "month",
|
|
2220
|
+
grants: [{ kind: "recurring", amount_cents: 2e4 }],
|
|
2221
|
+
trial_days: 14
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
var PREPAID = {
|
|
2225
|
+
kind: "prepaid",
|
|
2226
|
+
label: "Prepaid \u2014 $50 sign-up credit, then PAYG",
|
|
2227
|
+
description: "$50 one-time credit at signup. Once depleted the subscriber pays-as-they-go at $0.001/request by default.",
|
|
2228
|
+
pricing: {
|
|
2229
|
+
meters: [
|
|
2230
|
+
{ dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
|
|
2231
|
+
],
|
|
2232
|
+
recurring_fee_cents: 0,
|
|
2233
|
+
billing_interval: "month",
|
|
2234
|
+
grants: [{ kind: "one_time", amount_cents: 5e3 }],
|
|
2235
|
+
trial_days: 0
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
var METERED = {
|
|
2239
|
+
kind: "metered",
|
|
2240
|
+
label: "Metered (pay-as-you-go)",
|
|
2241
|
+
description: "No subscription fee. Pure usage billing at $0.001/request by default.",
|
|
2242
|
+
pricing: {
|
|
2243
|
+
meters: [
|
|
2244
|
+
{ dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
|
|
2245
|
+
],
|
|
2246
|
+
recurring_fee_cents: 0,
|
|
2247
|
+
billing_interval: "month",
|
|
2248
|
+
grants: [],
|
|
2249
|
+
trial_days: 0
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
var PLAN_PRESETS = Object.freeze({
|
|
2253
|
+
free: FREE,
|
|
2254
|
+
starter: STARTER,
|
|
2255
|
+
pro: PRO,
|
|
2256
|
+
prepaid: PREPAID,
|
|
2257
|
+
metered: METERED
|
|
1817
2258
|
});
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
applies_to: z14.enum(["request", "response", "both"]).default("request"),
|
|
2259
|
+
|
|
2260
|
+
// ../contracts/dist/plans/subscription-pricing-override.js
|
|
2261
|
+
import { z as z21 } from "zod";
|
|
2262
|
+
var subscriptionPricingOverrideSchema = z21.object({
|
|
2263
|
+
/** Override the plan's recurring fee for this subscriber. */
|
|
2264
|
+
recurring_fee_cents: z21.number().int().nonnegative().optional(),
|
|
1825
2265
|
/**
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
* are
|
|
2266
|
+
* Override the plan's credit grants. `grants[]` is the single credit
|
|
2267
|
+
* surface — when present, it fully replaces the plan's grants for this
|
|
2268
|
+
* subscriber (recurring + one-time credit are canonical entries here;
|
|
2269
|
+
* the legacy recurring/one-time scalar knobs were removed).
|
|
1829
2270
|
*/
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2271
|
+
grants: z21.array(grantSchema).max(40).optional(),
|
|
2272
|
+
/** Override the minimum-spend floor. */
|
|
2273
|
+
min_monthly_spend_cents: z21.number().int().nonnegative().optional(),
|
|
2274
|
+
/** Override the maximum-spend ceiling. */
|
|
2275
|
+
max_monthly_spend_cents: z21.number().int().nonnegative().optional(),
|
|
2276
|
+
/** Replace the entire meter list for this subscriber. When set, the
|
|
2277
|
+
* full plan meter array is replaced (not merged) — call it explicit
|
|
2278
|
+
* rather than implicit so a deal can also REMOVE billable meters,
|
|
2279
|
+
* not just adjust rates. */
|
|
2280
|
+
meters: z21.array(meterSchema).optional(),
|
|
2281
|
+
/** Free-text notes about the deal. Surfaced in admin UI for audit. */
|
|
2282
|
+
notes: z21.string().max(2e3).optional()
|
|
1834
2283
|
});
|
|
1835
|
-
var policyBodySchema = z14.discriminatedUnion("type", [
|
|
1836
|
-
z14.object({ type: z14.literal("rate_limit"), config: rateLimitConfigSchema }),
|
|
1837
|
-
z14.object({ type: z14.literal("auth"), config: authConfigSchema }),
|
|
1838
|
-
z14.object({ type: z14.literal("concurrency"), config: concurrencyConfigSchema }),
|
|
1839
|
-
z14.object({ type: z14.literal("retry"), config: retryConfigSchema }),
|
|
1840
|
-
z14.object({ type: z14.literal("transform"), config: transformConfigSchema })
|
|
1841
|
-
]);
|
|
1842
2284
|
|
|
1843
|
-
//
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
* (invariant #16).
|
|
1865
|
-
*/
|
|
1866
|
-
mutation_class: z15.enum(["runtime", "contractual"]).default("runtime"),
|
|
1867
|
-
cacheProfile: cacheProfileSchema
|
|
1868
|
-
}), policyBodySchema);
|
|
2285
|
+
// src/validate.ts
|
|
2286
|
+
function validateManifestIr(candidate) {
|
|
2287
|
+
const parsed = manifestIrSchema.safeParse(candidate);
|
|
2288
|
+
if (!parsed.success) {
|
|
2289
|
+
return {
|
|
2290
|
+
ok: false,
|
|
2291
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
2292
|
+
code: issue.code.toUpperCase(),
|
|
2293
|
+
path: issue.path.map(String).join("."),
|
|
2294
|
+
message: issue.message
|
|
2295
|
+
}))
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
const ir = JSON.parse(
|
|
2299
|
+
JSON.stringify(candidate)
|
|
2300
|
+
);
|
|
2301
|
+
return { ok: true, ir, irHash: hashIr(ir) };
|
|
2302
|
+
}
|
|
2303
|
+
function hashIr(ir) {
|
|
2304
|
+
return hashManifestIr(ir);
|
|
2305
|
+
}
|
|
1869
2306
|
|
|
1870
|
-
//
|
|
1871
|
-
|
|
2307
|
+
// src/version.ts
|
|
2308
|
+
var SDK_VERSION = true ? "0.6.0" : "0.0.0-dev";
|
|
1872
2309
|
|
|
1873
|
-
//
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
resource: z16.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/),
|
|
1884
|
-
effect: z16.enum(["create", "delete"])
|
|
1885
|
-
});
|
|
1886
|
-
var actionSpecSchema = z16.object({
|
|
1887
|
-
id: z16.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/),
|
|
1888
|
-
title: z16.string().min(1).max(160).optional(),
|
|
1889
|
-
kind: actionKindSchema,
|
|
1890
|
-
actorType: z16.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/).optional(),
|
|
1891
|
-
subject: actionSubjectBindingSchema.optional(),
|
|
1892
|
-
inputSchemaRef: z16.string().min(1).max(240).optional(),
|
|
1893
|
-
audit: actionAuditPolicySchema.default("metadata"),
|
|
1894
|
-
resource: actionResourceEffectSchema.optional()
|
|
1895
|
-
});
|
|
2310
|
+
// src/refs.ts
|
|
2311
|
+
function isCapabilityGrant(value) {
|
|
2312
|
+
return typeof value === "object" && value !== null && value.kind === "capability_grant";
|
|
2313
|
+
}
|
|
2314
|
+
function keyOf(ref) {
|
|
2315
|
+
return typeof ref === "string" ? ref : ref.key;
|
|
2316
|
+
}
|
|
2317
|
+
function displayFromKey(key) {
|
|
2318
|
+
return key.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2319
|
+
}
|
|
1896
2320
|
|
|
1897
|
-
//
|
|
1898
|
-
var
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2321
|
+
// src/backend.ts
|
|
2322
|
+
var BACKEND_ID_PATTERN2 = /^[a-z0-9][a-z0-9_-]*$/;
|
|
2323
|
+
function createBackendNode(id, options = {}) {
|
|
2324
|
+
if (!BACKEND_ID_PATTERN2.test(id)) {
|
|
2325
|
+
throw new ManifestBuilderError(
|
|
2326
|
+
`backend "${id}": id must be a lowercase slug ([a-z0-9][a-z0-9_-]*)`
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
return {
|
|
2330
|
+
id,
|
|
2331
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
2332
|
+
...options.slug !== void 0 ? { slug: options.slug } : {},
|
|
2333
|
+
...options.transport !== void 0 ? {
|
|
2334
|
+
transport: {
|
|
2335
|
+
...options.transport.mode !== void 0 ? { mode: options.transport.mode } : {},
|
|
2336
|
+
...options.transport.runner !== void 0 ? { runner: options.transport.runner } : {}
|
|
2337
|
+
}
|
|
2338
|
+
} : {},
|
|
2339
|
+
...options.verification !== void 0 ? { verification: options.verification } : {},
|
|
2340
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
|
|
2341
|
+
...options.default !== void 0 ? { default: options.default } : {},
|
|
2342
|
+
...options.originUrl !== void 0 ? { originUrl: options.originUrl } : {},
|
|
2343
|
+
...options.originHostname !== void 0 ? { originHostname: options.originHostname } : {}
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function buildBackendBlock(backendNodes) {
|
|
2347
|
+
const out = {};
|
|
2348
|
+
for (const node of [...backendNodes].sort(
|
|
2349
|
+
(a, b) => a.id.localeCompare(b.id)
|
|
2350
|
+
)) {
|
|
2351
|
+
const { id, ...definition } = node;
|
|
2352
|
+
out[id] = definition;
|
|
2353
|
+
}
|
|
2354
|
+
return out;
|
|
2355
|
+
}
|
|
2356
|
+
function assertBackendBindingsValid(files, backendNodes, meterDefinitions) {
|
|
2357
|
+
if (backendNodes.length === 0) return;
|
|
2358
|
+
const byId = new Map(backendNodes.map((backend) => [backend.id, backend]));
|
|
2359
|
+
const declaredMeters = new Set(meterDefinitions.map((meter) => meter.key));
|
|
2360
|
+
for (const backend of backendNodes) {
|
|
2361
|
+
for (const meter of backend.meters ?? []) {
|
|
2362
|
+
if (declaredMeters.has(meter)) continue;
|
|
2363
|
+
throw new ManifestBuilderError(
|
|
2364
|
+
`UNKNOWN_METER_IN_BACKEND: backend "${backend.id}" allows meter "${meter}" but it is not declared \u2014 call product.meter("${meter}", ...) first`
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
const ids = backendNodes.map((backend) => backend.id);
|
|
2369
|
+
const explicitDefaults = backendNodes.filter(
|
|
2370
|
+
(backend) => backend.default === true
|
|
2371
|
+
);
|
|
2372
|
+
const defaultId = ids.length === 1 ? ids[0] : explicitDefaults.length === 1 ? explicitDefaults[0].id : null;
|
|
2373
|
+
const ambiguous = defaultId === null;
|
|
2374
|
+
for (const file of files) {
|
|
2375
|
+
const fileBinding = file.backend;
|
|
2376
|
+
if (fileBinding !== void 0 && !byId.has(fileBinding)) {
|
|
2377
|
+
throw new ManifestBuilderError(
|
|
2378
|
+
`UNKNOWN_BACKEND_IN_ROUTE: feature "${file.feature}" binds backend "${fileBinding}" which is not declared \u2014 call product.backend("${fileBinding}", ...) first`
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
file.routes.forEach((route, routeIndex) => {
|
|
2382
|
+
const explicit = route.backend ?? fileBinding;
|
|
2383
|
+
if (explicit !== void 0) {
|
|
2384
|
+
if (!byId.has(explicit)) {
|
|
2385
|
+
throw new ManifestBuilderError(
|
|
2386
|
+
`UNKNOWN_BACKEND_IN_ROUTE: feature "${file.feature}" route ${routeIndex} binds backend "${explicit}" which is not declared \u2014 call product.backend("${explicit}", ...) first`
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
} else if (ambiguous) {
|
|
2390
|
+
throw new ManifestBuilderError(
|
|
2391
|
+
`AMBIGUOUS_DEFAULT_BACKEND: feature "${file.feature}" route ${routeIndex} has no backend binding and the product declares ${ids.length} backends with no single default \u2014 mark exactly one backend \`default: true\` or bind the route explicitly`
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
const boundId = explicit ?? defaultId;
|
|
2395
|
+
if (!boundId) return;
|
|
2396
|
+
const backend = byId.get(boundId);
|
|
2397
|
+
const allow = backend?.meters;
|
|
2398
|
+
if (!allow) return;
|
|
2399
|
+
const allowed = new Set(allow);
|
|
2400
|
+
const routeMeters = /* @__PURE__ */ new Set([
|
|
2401
|
+
...Object.keys(route.metering?.defaults ?? {}),
|
|
2402
|
+
...route.metering?.reports ?? [],
|
|
2403
|
+
...Object.keys(route.metering?.estimates ?? {})
|
|
2404
|
+
]);
|
|
2405
|
+
for (const meter of routeMeters) {
|
|
2406
|
+
if (allowed.has(meter)) continue;
|
|
2407
|
+
throw new ManifestBuilderError(
|
|
2408
|
+
`ROUTE_METER_NOT_ALLOWED_BY_BACKEND: feature "${file.feature}" route ${routeIndex} meters "${meter}" but backend "${boundId}" does not allow it (backend.meters = [${allow.join(", ")}])`
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
1977
2414
|
|
|
1978
|
-
//
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
})
|
|
2087
|
-
});
|
|
2088
|
-
var runtimeMigrationsFileSchema = z18.object({
|
|
2089
|
-
cacheProfile: cacheProfileSchema.default("blocking"),
|
|
2090
|
-
migrations: z18.record(keyNameSchema, migrationTriggerSchema).default({})
|
|
2091
|
-
});
|
|
2415
|
+
// src/product-assembly.ts
|
|
2416
|
+
function assembleProductSpec(input) {
|
|
2417
|
+
const { options } = input;
|
|
2418
|
+
const base = {
|
|
2419
|
+
product: {
|
|
2420
|
+
name: input.name,
|
|
2421
|
+
baseUrl: options.origin,
|
|
2422
|
+
...options.displayName !== void 0 ? { displayName: options.displayName } : {},
|
|
2423
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2424
|
+
...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
|
|
2425
|
+
...options.visibility !== void 0 ? { visibility: options.visibility } : {},
|
|
2426
|
+
...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
|
|
2427
|
+
...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
|
|
2428
|
+
...options.envBranchPrefix !== void 0 ? { envBranchPrefix: options.envBranchPrefix } : {}
|
|
2429
|
+
},
|
|
2430
|
+
gateway: {
|
|
2431
|
+
...options.authHeader !== void 0 ? { authHeader: options.authHeader } : {},
|
|
2432
|
+
...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
|
|
2433
|
+
},
|
|
2434
|
+
metering: {
|
|
2435
|
+
meters: buildMeterDefinitions(
|
|
2436
|
+
input.meters,
|
|
2437
|
+
input.defaultMeterCosts,
|
|
2438
|
+
input.featureLayers,
|
|
2439
|
+
input.workflows
|
|
2440
|
+
),
|
|
2441
|
+
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2442
|
+
},
|
|
2443
|
+
...input.backends.length ? { backend: buildBackendBlock(input.backends) } : {},
|
|
2444
|
+
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2445
|
+
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
2446
|
+
...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
|
|
2447
|
+
...input.surfaces.length ? { surfaces: sortBy2(input.surfaces, (surface) => surface.key) } : {},
|
|
2448
|
+
...input.entitlements.length ? {
|
|
2449
|
+
entitlements: sortBy2(
|
|
2450
|
+
input.entitlements,
|
|
2451
|
+
(entitlement) => entitlement.key
|
|
2452
|
+
)
|
|
2453
|
+
} : {},
|
|
2454
|
+
...input.workflows.length ? { workflows: sortBy2(input.workflows, (workflow) => workflow.key) } : {},
|
|
2455
|
+
...input.frontendManifest !== void 0 ? { frontend: input.frontendManifest } : {},
|
|
2456
|
+
...input.migrations.length ? { migrations: sortBy2(input.migrations, (migration) => migration.id) } : {},
|
|
2457
|
+
...input.resources.length ? { resources: sortBy2(input.resources, (resource) => resource.name) } : {},
|
|
2458
|
+
plans: sortBy2(input.plans, (plan) => plan.key)
|
|
2459
|
+
};
|
|
2460
|
+
return mergeProductPatch(
|
|
2461
|
+
base,
|
|
2462
|
+
input.productPatch
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
function buildMeterDefinitions(meters, defaultMeterCosts, featureLayers, workflows) {
|
|
2466
|
+
const routeValueMeters = routeValueMeterKeys(
|
|
2467
|
+
defaultMeterCosts,
|
|
2468
|
+
featureLayers,
|
|
2469
|
+
workflows
|
|
2470
|
+
);
|
|
2471
|
+
return sortBy2(meters, (meter) => meter.key).map((meter) => {
|
|
2472
|
+
if (meter.aggregation !== void 0) return meter;
|
|
2473
|
+
if (!routeValueMeters.has(meter.key)) return meter;
|
|
2474
|
+
return { ...meter, aggregation: "SUM" };
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
function routeValueMeterKeys(defaultMeterCosts, featureLayers, workflows) {
|
|
2478
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2479
|
+
for (const cost of defaultMeterCosts) {
|
|
2480
|
+
keys.add(cost.meter);
|
|
2481
|
+
}
|
|
2482
|
+
for (const file of featureLayers) {
|
|
2483
|
+
for (const route of file.routes) {
|
|
2484
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
2485
|
+
keys.add(meter);
|
|
2486
|
+
}
|
|
2487
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2488
|
+
keys.add(meter);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
for (const workflow of workflows) {
|
|
2493
|
+
for (const meter of workflow.meters ?? []) keys.add(meter);
|
|
2494
|
+
for (const meter of Object.keys(workflow.estimates ?? {})) keys.add(meter);
|
|
2495
|
+
}
|
|
2496
|
+
return keys;
|
|
2497
|
+
}
|
|
2498
|
+
function buildCustomerContext(options) {
|
|
2499
|
+
return {
|
|
2500
|
+
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
2501
|
+
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
2502
|
+
...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function sortBy2(items, key) {
|
|
2506
|
+
return [...items].sort((a, b) => key(a).localeCompare(key(b)));
|
|
2507
|
+
}
|
|
2508
|
+
function isPlainObject(value) {
|
|
2509
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2510
|
+
}
|
|
2511
|
+
function mergeProductPatch(base, patch) {
|
|
2512
|
+
const out = { ...base };
|
|
2513
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
2514
|
+
const existing = out[key];
|
|
2515
|
+
if (isPlainObject(existing) && isPlainObject(value)) {
|
|
2516
|
+
out[key] = mergeProductPatch(existing, value);
|
|
2517
|
+
} else {
|
|
2518
|
+
out[key] = value;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
return out;
|
|
2522
|
+
}
|
|
2092
2523
|
|
|
2093
|
-
//
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
}
|
|
2524
|
+
// src/frontend.ts
|
|
2525
|
+
function createFrontendManifest() {
|
|
2526
|
+
return { version: 1, nav: [], pages: [] };
|
|
2527
|
+
}
|
|
2528
|
+
function setFrontendNav(manifest, items) {
|
|
2529
|
+
manifest.nav = items.map((item) => ({
|
|
2530
|
+
label: item.label,
|
|
2531
|
+
path: item.path,
|
|
2532
|
+
...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
|
|
2533
|
+
}));
|
|
2534
|
+
}
|
|
2535
|
+
function addFrontendPage(manifest, path, options) {
|
|
2536
|
+
if (manifest.pages?.some((page) => page.path === path)) {
|
|
2537
|
+
throw new ManifestBuilderError(
|
|
2538
|
+
`duplicate frontend page "${path}" \u2014 each frontend page path must be declared once`
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
manifest.pages ??= [];
|
|
2542
|
+
manifest.pages.push({
|
|
2543
|
+
path,
|
|
2544
|
+
title: options.title,
|
|
2545
|
+
requiresAuth: options.requiresAuth,
|
|
2546
|
+
...options.capability !== void 0 ? { capability: keyOf(options.capability) } : {},
|
|
2547
|
+
...options.components?.length ? {
|
|
2548
|
+
components: options.components.map((component) => ({
|
|
2549
|
+
component: component.component,
|
|
2550
|
+
...component.props !== void 0 ? { props: component.props } : {},
|
|
2551
|
+
...component.capability !== void 0 ? { capability: keyOf(component.capability) } : {},
|
|
2552
|
+
...component.gateMode !== void 0 ? { gateMode: component.gateMode } : {}
|
|
2553
|
+
}))
|
|
2554
|
+
} : {}
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
function frontendCapabilityKeys(manifest) {
|
|
2558
|
+
return [
|
|
2559
|
+
...(manifest.nav ?? []).flatMap(
|
|
2560
|
+
(item) => item.capability ? [item.capability] : []
|
|
2561
|
+
),
|
|
2562
|
+
...(manifest.pages ?? []).flatMap((page) => [
|
|
2563
|
+
...page.capability ? [page.capability] : [],
|
|
2564
|
+
...(page.components ?? []).flatMap(
|
|
2565
|
+
(component) => component.capability ? [component.capability] : []
|
|
2566
|
+
)
|
|
2567
|
+
])
|
|
2568
|
+
];
|
|
2569
|
+
}
|
|
2110
2570
|
|
|
2111
|
-
//
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
displayName: z20.string().max(200).optional(),
|
|
2197
|
-
description: z20.string().max(2e3).optional(),
|
|
2198
|
-
baseUrl: z20.string().url("baseUrl must be a valid URL"),
|
|
2199
|
-
sandboxBaseUrl: z20.string().url("sandboxBaseUrl must be a valid URL").optional(),
|
|
2200
|
-
visibility: z20.enum(["public", "private"]).default("public"),
|
|
2201
|
-
logoUrl: z20.string().url().optional(),
|
|
2202
|
-
primaryColor: z20.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
2203
|
-
envBranchPrefix: z20.string().max(50).nullable().optional()
|
|
2204
|
-
}),
|
|
2205
|
-
/**
|
|
2206
|
-
* Meter catalog. Referenced by route `metering` metadata.
|
|
2207
|
-
* Same shape as the legacy meterDefinitionSchema (re-exported from
|
|
2208
|
-
* product.ts to keep one source of truth during the transition).
|
|
2209
|
-
*
|
|
2210
|
-
* Lifted to a top-level array so the compiler can hash + invalidate
|
|
2211
|
-
* meter changes independently from plan changes (incremental
|
|
2212
|
-
* compilation per invariant #1 + Merkle DAG).
|
|
2213
|
-
*/
|
|
2214
|
-
meters: z20.array(meterDefinitionSchema).max(10).default([]),
|
|
2215
|
-
/**
|
|
2216
|
-
* Bill on 4xx responses (independent of meter setup). Stays on the
|
|
2217
|
-
* product spec because it's a contractual choice that affects
|
|
2218
|
-
* billing predictability.
|
|
2219
|
-
*/
|
|
2220
|
-
billOn4xx: z20.boolean().default(false),
|
|
2221
|
-
frontend: frontendManifestSchema.optional(),
|
|
2222
|
-
migrations: migrationDeclsSchema.optional(),
|
|
2223
|
-
resources: countedResourcesSchema,
|
|
2224
|
-
policies: productOperatorPoliciesSchema,
|
|
2225
|
-
customer_context: productCustomerContextSchema.optional(),
|
|
2226
|
-
surfaces: productSurfacesSchema,
|
|
2227
|
-
entitlements: productEntitlementsSchema,
|
|
2228
|
-
workflows: productWorkflowsSchema,
|
|
2229
|
-
billing: z20.object({
|
|
2230
|
-
gracePeriodDays: z20.number().int().nonnegative().default(3),
|
|
2231
|
-
// When true (default), a plan limit INCREASE re-projects onto active
|
|
2232
|
-
// subscribers immediately; a DECREASE always defers to period end.
|
|
2233
|
-
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
2234
|
-
applyLimitUpgradesInstantly: z20.boolean().optional(),
|
|
2235
|
-
subscriberChangePolicy: subscriberChangePolicySchema.default(DEFAULT_SUBSCRIBER_CHANGE_POLICY)
|
|
2236
|
-
}).default({
|
|
2237
|
-
gracePeriodDays: 3,
|
|
2238
|
-
subscriberChangePolicy: DEFAULT_SUBSCRIBER_CHANGE_POLICY
|
|
2239
|
-
}),
|
|
2240
|
-
plans: z20.array(planSpecV2Schema).max(4).default([]),
|
|
2241
|
-
add_ons: addOnsBlockSchema,
|
|
2242
|
-
lifecycle: z20.object({
|
|
2243
|
-
breaking_changes: z20.object({
|
|
2244
|
-
require_deprecation_window_days: z20.number().int().nonnegative().default(0),
|
|
2245
|
-
require_successor_route: z20.boolean().default(false)
|
|
2246
|
-
}).default({
|
|
2247
|
-
require_deprecation_window_days: 0,
|
|
2248
|
-
require_successor_route: false
|
|
2249
|
-
})
|
|
2250
|
-
}).default({
|
|
2251
|
-
breaking_changes: {
|
|
2252
|
-
require_deprecation_window_days: 0,
|
|
2253
|
-
require_successor_route: false
|
|
2571
|
+
// src/route-metering.ts
|
|
2572
|
+
function parseRouteMatch(match) {
|
|
2573
|
+
const trimmed = match.trim();
|
|
2574
|
+
const space = trimmed.indexOf(" ");
|
|
2575
|
+
if (space === -1) {
|
|
2576
|
+
if (!trimmed.startsWith("/")) {
|
|
2577
|
+
throw new ManifestBuilderError(
|
|
2578
|
+
`route "${match}": expected "METHOD /path" or "/path"`
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
return { method: "*", path: trimmed };
|
|
2582
|
+
}
|
|
2583
|
+
const method = trimmed.slice(0, space).toUpperCase();
|
|
2584
|
+
const path = trimmed.slice(space + 1).trim();
|
|
2585
|
+
const methods = [
|
|
2586
|
+
"GET",
|
|
2587
|
+
"POST",
|
|
2588
|
+
"PUT",
|
|
2589
|
+
"PATCH",
|
|
2590
|
+
"DELETE",
|
|
2591
|
+
"HEAD",
|
|
2592
|
+
"OPTIONS",
|
|
2593
|
+
"*"
|
|
2594
|
+
];
|
|
2595
|
+
if (!methods.includes(method)) {
|
|
2596
|
+
throw new ManifestBuilderError(
|
|
2597
|
+
`route "${match}": unknown HTTP method "${method}"`
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
if (!path.startsWith("/")) {
|
|
2601
|
+
throw new ManifestBuilderError(
|
|
2602
|
+
`route "${match}": path must start with "/"`
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
return { method, path };
|
|
2606
|
+
}
|
|
2607
|
+
function makeMeterRef(key) {
|
|
2608
|
+
return {
|
|
2609
|
+
kind: "meter",
|
|
2610
|
+
key,
|
|
2611
|
+
fixed: (value) => ({ kind: "meter_cost", meter: key, value }),
|
|
2612
|
+
estimate: (value) => ({ kind: "meter_estimate", meter: key, value })
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
function normalizeMeterCost(cost, hasMeter) {
|
|
2616
|
+
if (!cost || cost.kind !== "meter_cost") {
|
|
2617
|
+
throw new ManifestBuilderError(
|
|
2618
|
+
"meter cost must be created by meter.fixed(value)"
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
if (!hasMeter(cost.meter)) {
|
|
2622
|
+
throw new ManifestBuilderError(
|
|
2623
|
+
`meter cost references unknown meter "${cost.meter}"`
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
if (!Number.isFinite(cost.value) || cost.value < 0) {
|
|
2627
|
+
throw new ManifestBuilderError(
|
|
2628
|
+
`meter "${cost.meter}" fixed value must be a non-negative finite number`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
return cost;
|
|
2632
|
+
}
|
|
2633
|
+
function normalizeMeterCosts(costs, normalize) {
|
|
2634
|
+
if (!costs) return [];
|
|
2635
|
+
const entries = Array.isArray(costs) ? costs : [costs];
|
|
2636
|
+
return entries.map((cost) => normalize(cost));
|
|
2637
|
+
}
|
|
2638
|
+
function normalizeMeterRefs(refs) {
|
|
2639
|
+
const entries = Array.isArray(refs) ? refs : [refs];
|
|
2640
|
+
return entries.map(keyOf);
|
|
2641
|
+
}
|
|
2642
|
+
function statusPolicyPartIsValid2(part) {
|
|
2643
|
+
const trimmed = part.trim();
|
|
2644
|
+
if (!trimmed) return false;
|
|
2645
|
+
const [startRaw, endRaw, extra] = trimmed.split("-");
|
|
2646
|
+
if (extra !== void 0 || !/^\d{3}$/.test(startRaw ?? "")) return false;
|
|
2647
|
+
const start = Number(startRaw);
|
|
2648
|
+
const end = endRaw === void 0 ? start : Number(endRaw);
|
|
2649
|
+
if (endRaw !== void 0 && !/^\d{3}$/.test(endRaw)) return false;
|
|
2650
|
+
return start >= 100 && start <= 599 && end >= 100 && end <= 599 && start <= end;
|
|
2651
|
+
}
|
|
2652
|
+
function assertStatusCodePolicy(value) {
|
|
2653
|
+
if (Array.isArray(value)) {
|
|
2654
|
+
if (value.length === 0) {
|
|
2655
|
+
throw new ManifestBuilderError("onStatusCodes cannot be an empty array");
|
|
2254
2656
|
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
/** Version of @farthershore/product that emitted this envelope. */
|
|
2270
|
-
sdkVersion: z21.string().min(1).max(64),
|
|
2271
|
-
/** Legacy unified ProductSpec — the live `CompileProductOptions.sourceSpec`. */
|
|
2272
|
-
product: productSpecSchema,
|
|
2273
|
-
/** One entry per feature, sorted by `feature` (mirrors /routes/<feature>.yaml). */
|
|
2274
|
-
routes: z21.array(routesFileSchema).max(200).default([]),
|
|
2275
|
-
/** Sorted by `name` (mirrors /policies/<name>.yaml). */
|
|
2276
|
-
policies: z21.array(policyFileSchema).max(200).default([]),
|
|
2277
|
-
/** Sorted by `capability` (mirrors /capabilities/<name>.yaml). */
|
|
2278
|
-
capabilities: z21.array(capabilityFileSchema).max(200).default([]),
|
|
2279
|
-
/** Reserved. Always null at irVersion 1 — runtime stays YAML/dashboard. */
|
|
2280
|
-
runtime: z21.object({
|
|
2281
|
-
rollout: z21.null().default(null),
|
|
2282
|
-
flags: z21.null().default(null),
|
|
2283
|
-
migrations: z21.null().default(null)
|
|
2284
|
-
}).default({ rollout: null, flags: null, migrations: null })
|
|
2285
|
-
});
|
|
2286
|
-
function canonicalManifestJson(value) {
|
|
2287
|
-
return stableJson(JSON.parse(JSON.stringify(value)));
|
|
2657
|
+
for (const status of value) {
|
|
2658
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
2659
|
+
throw new ManifestBuilderError(
|
|
2660
|
+
`onStatusCodes array contains invalid HTTP status "${status}"`
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (!value.split(",").every(statusPolicyPartIsValid2)) {
|
|
2667
|
+
throw new ManifestBuilderError(
|
|
2668
|
+
'onStatusCodes must be comma-separated HTTP status codes or numeric ranges, e.g. "200-299,304"'
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2288
2671
|
}
|
|
2289
|
-
function
|
|
2290
|
-
|
|
2672
|
+
function assertEstimateValue(meter, value) {
|
|
2673
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2674
|
+
throw new ManifestBuilderError(
|
|
2675
|
+
`meter "${meter}" estimate must be a non-negative finite number`
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2291
2678
|
}
|
|
2292
|
-
function
|
|
2293
|
-
if (
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2679
|
+
function normalizeMeterEstimate(estimate, hasMeter) {
|
|
2680
|
+
if (!estimate || estimate.kind !== "meter_estimate") {
|
|
2681
|
+
throw new ManifestBuilderError(
|
|
2682
|
+
"meter estimate must be created by meter.estimate(value)"
|
|
2683
|
+
);
|
|
2297
2684
|
}
|
|
2298
|
-
|
|
2685
|
+
if (!hasMeter(estimate.meter)) {
|
|
2686
|
+
throw new ManifestBuilderError(
|
|
2687
|
+
`meter estimate references unknown meter "${estimate.meter}"`
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
assertEstimateValue(estimate.meter, estimate.value);
|
|
2691
|
+
return estimate;
|
|
2299
2692
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2693
|
+
function isMeterEstimate(value) {
|
|
2694
|
+
return typeof value === "object" && value !== null && value.kind === "meter_estimate";
|
|
2695
|
+
}
|
|
2696
|
+
function normalizeMeterEstimates(estimates, hasMeter) {
|
|
2697
|
+
if (!estimates) return {};
|
|
2698
|
+
if (Array.isArray(estimates)) {
|
|
2699
|
+
return Object.fromEntries(
|
|
2700
|
+
estimates.map((estimate) => {
|
|
2701
|
+
const normalized = normalizeMeterEstimate(estimate, hasMeter);
|
|
2702
|
+
return [normalized.meter, normalized.value];
|
|
2703
|
+
})
|
|
2704
|
+
);
|
|
2312
2705
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
label: "Starter \u2014 $20/mo + $20 included",
|
|
2317
|
-
description: "$20/month subscription with $20 of usage credit each period. Stripe-style minimum-spend; overage billed at $0.001/request by default.",
|
|
2318
|
-
pricing: {
|
|
2319
|
-
meters: [
|
|
2320
|
-
{ dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
|
|
2321
|
-
],
|
|
2322
|
-
recurring_fee_cents: 2e3,
|
|
2323
|
-
billing_interval: "month",
|
|
2324
|
-
grants: [{ kind: "recurring", amount_cents: 2e3 }],
|
|
2325
|
-
trial_days: 0
|
|
2706
|
+
if (isMeterEstimate(estimates)) {
|
|
2707
|
+
const normalized = normalizeMeterEstimate(estimates, hasMeter);
|
|
2708
|
+
return { [normalized.meter]: normalized.value };
|
|
2326
2709
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
label: "Pro \u2014 $100/mo + $200 included",
|
|
2331
|
-
description: "$100/month subscription with $200 of usage credit each period and a 14-day trial. Overage billed at $0.0005/request by default.",
|
|
2332
|
-
pricing: {
|
|
2333
|
-
meters: [
|
|
2334
|
-
{ dimension: "requests", kind: "linear", price_per_unit_micros: 500 }
|
|
2335
|
-
],
|
|
2336
|
-
recurring_fee_cents: 1e4,
|
|
2337
|
-
billing_interval: "month",
|
|
2338
|
-
grants: [{ kind: "recurring", amount_cents: 2e4 }],
|
|
2339
|
-
trial_days: 14
|
|
2710
|
+
const estimateMap = estimates;
|
|
2711
|
+
for (const [meter, value] of Object.entries(estimateMap)) {
|
|
2712
|
+
assertEstimateValue(meter, value);
|
|
2340
2713
|
}
|
|
2341
|
-
};
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2714
|
+
return { ...estimateMap };
|
|
2715
|
+
}
|
|
2716
|
+
function buildRouteDefinition(match, options, deps) {
|
|
2717
|
+
const parsed = parseRouteMatch(match);
|
|
2718
|
+
const metering = buildRouteMetering(options, deps);
|
|
2719
|
+
return {
|
|
2720
|
+
match: parsed,
|
|
2721
|
+
...metering ? { metering } : {},
|
|
2722
|
+
...options.unmetered !== void 0 ? { unmetered: options.unmetered } : {},
|
|
2723
|
+
...options.inheritDefaultMeters !== void 0 ? { inheritDefaultMeters: options.inheritDefaultMeters } : {},
|
|
2724
|
+
...options.action !== void 0 ? { action: keyOf(options.action) } : {},
|
|
2725
|
+
...options.backend !== void 0 ? { backend: keyOf(options.backend) } : {}
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
function buildRouteMetering(options, deps) {
|
|
2729
|
+
if (options.unmetered === true) return void 0;
|
|
2730
|
+
const defaults = {};
|
|
2731
|
+
for (const cost of normalizeMeterCosts(
|
|
2732
|
+
options.costs,
|
|
2733
|
+
(cost2) => deps.normalizeMeterCost(cost2)
|
|
2734
|
+
)) {
|
|
2735
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2736
|
+
}
|
|
2737
|
+
const reports = [...new Set(normalizeMeterRefs(options.reports ?? []))];
|
|
2738
|
+
const estimates = {};
|
|
2739
|
+
for (const meter of reports) {
|
|
2740
|
+
const definition = deps.getMeterDefinition(meter);
|
|
2741
|
+
if (typeof definition?.estimate === "number") {
|
|
2742
|
+
assertEstimateValue(meter, definition.estimate);
|
|
2743
|
+
estimates[meter] = definition.estimate;
|
|
2744
|
+
}
|
|
2354
2745
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2746
|
+
const explicitEstimates = normalizeMeterEstimates(
|
|
2747
|
+
options.estimates,
|
|
2748
|
+
(meter) => deps.getMeterDefinition(meter) !== void 0
|
|
2749
|
+
);
|
|
2750
|
+
for (const [meter, value] of Object.entries(explicitEstimates)) {
|
|
2751
|
+
estimates[meter] = value;
|
|
2752
|
+
}
|
|
2753
|
+
const out = {};
|
|
2754
|
+
if (Object.keys(defaults).length) out.defaults = defaults;
|
|
2755
|
+
if (reports.length) out.reports = reports;
|
|
2756
|
+
if (Object.keys(estimates).length) out.estimates = estimates;
|
|
2757
|
+
if (options.onStatusCodes !== void 0) {
|
|
2758
|
+
assertStatusCodePolicy(options.onStatusCodes);
|
|
2759
|
+
out.onStatusCodes = options.onStatusCodes;
|
|
2760
|
+
}
|
|
2761
|
+
return Object.keys(out).length ? out : void 0;
|
|
2762
|
+
}
|
|
2763
|
+
function materializeRoute(route, defaultMeterCosts) {
|
|
2764
|
+
if (route.unmetered === true) return route;
|
|
2765
|
+
const defaults = {
|
|
2766
|
+
...route.metering?.defaults ?? {}
|
|
2767
|
+
};
|
|
2768
|
+
if (route.inheritDefaultMeters !== false) {
|
|
2769
|
+
for (const cost of defaultMeterCosts) {
|
|
2770
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2771
|
+
}
|
|
2368
2772
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
}
|
|
2773
|
+
const hasMetering = route.metering !== void 0 || Object.keys(defaults).length > 0;
|
|
2774
|
+
const metering = hasMetering ? {
|
|
2775
|
+
...route.metering ?? {},
|
|
2776
|
+
...Object.keys(defaults).length ? { defaults } : {}
|
|
2777
|
+
} : void 0;
|
|
2778
|
+
return {
|
|
2779
|
+
...route,
|
|
2780
|
+
...metering ? { metering } : {}
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
function routeMeterDependencyKeys(route) {
|
|
2784
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2785
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
2786
|
+
keys.add(meter);
|
|
2787
|
+
}
|
|
2788
|
+
for (const meter of route.metering?.reports ?? []) keys.add(meter);
|
|
2789
|
+
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
2790
|
+
keys.add(meter);
|
|
2791
|
+
}
|
|
2792
|
+
return [...keys];
|
|
2793
|
+
}
|
|
2794
|
+
function assertRouteMeteringValid(files, meterDefinitions) {
|
|
2795
|
+
const definitions = new Map(
|
|
2796
|
+
Array.from(meterDefinitions, (meter) => [meter.key, meter])
|
|
2797
|
+
);
|
|
2798
|
+
for (const file of files) {
|
|
2799
|
+
file.routes.forEach((route, routeIndex) => {
|
|
2800
|
+
if (route.unmetered === true) return;
|
|
2801
|
+
const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
|
|
2802
|
+
const reports = new Set(route.metering?.reports ?? []);
|
|
2803
|
+
for (const meter of defaults) {
|
|
2804
|
+
if (definitions.has(meter)) continue;
|
|
2805
|
+
throw new ManifestBuilderError(
|
|
2806
|
+
`feature "${file.feature}" route ${routeIndex}: fixed meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2810
|
+
if (!definitions.has(meter)) {
|
|
2811
|
+
throw new ManifestBuilderError(
|
|
2812
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
if (defaults.has(meter)) {
|
|
2816
|
+
throw new ManifestBuilderError(
|
|
2817
|
+
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2822
|
+
const definition = definitions.get(meter);
|
|
2823
|
+
const enforcement = definition?.enforcementType ?? "estimated_then_settled";
|
|
2824
|
+
const hasEstimate = route.metering?.estimates && Object.prototype.hasOwnProperty.call(route.metering.estimates, meter);
|
|
2825
|
+
if ((enforcement === "exact_pre_request" || enforcement === "estimated_then_settled") && !hasEstimate) {
|
|
2826
|
+
throw new ManifestBuilderError(
|
|
2827
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" needs an estimate for gateway admission`
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
for (const [meter, value] of Object.entries(
|
|
2832
|
+
route.metering?.estimates ?? {}
|
|
2833
|
+
)) {
|
|
2834
|
+
assertEstimateValue(meter, value);
|
|
2835
|
+
if (!definitions.has(meter)) {
|
|
2836
|
+
throw new ManifestBuilderError(
|
|
2837
|
+
`feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
if (reports.has(meter)) continue;
|
|
2841
|
+
throw new ManifestBuilderError(
|
|
2842
|
+
`feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" must also be listed in reports because estimates are pre-request reservations for dynamic usage`
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2377
2848
|
|
|
2378
|
-
//
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
}
|
|
2849
|
+
// src/dependencies.ts
|
|
2850
|
+
function capabilityDependsOn(file) {
|
|
2851
|
+
return [
|
|
2852
|
+
...dependenciesFor("feature", file.includes_features),
|
|
2853
|
+
...dependenciesFor("policy", file.includes_policies),
|
|
2854
|
+
...dependenciesFor("capability", file.includes_capabilities)
|
|
2855
|
+
];
|
|
2856
|
+
}
|
|
2857
|
+
function policyDependsOn(file) {
|
|
2858
|
+
return dependenciesFor("meter", file.compatible_with?.meters);
|
|
2859
|
+
}
|
|
2860
|
+
function entitlementDependsOn(entitlement, hasResource) {
|
|
2861
|
+
const limitDimensions = (entitlement.limits ?? []).map(
|
|
2862
|
+
(limit) => limit.dimension
|
|
2863
|
+
);
|
|
2864
|
+
return [
|
|
2865
|
+
...dependenciesFor("capability", entitlement.capabilities),
|
|
2866
|
+
...existingDependenciesFor(
|
|
2867
|
+
"meter",
|
|
2868
|
+
[...entitlement.meters ?? [], ...limitDimensions],
|
|
2869
|
+
hasResource
|
|
2870
|
+
)
|
|
2871
|
+
];
|
|
2872
|
+
}
|
|
2873
|
+
function workflowDependsOn(workflow) {
|
|
2874
|
+
return [
|
|
2875
|
+
...dependenciesFor("capability", workflow.capabilities),
|
|
2876
|
+
...dependenciesFor("meter", [
|
|
2877
|
+
...workflow.meters ?? [],
|
|
2878
|
+
...Object.keys(workflow.estimates ?? {})
|
|
2879
|
+
])
|
|
2880
|
+
];
|
|
2881
|
+
}
|
|
2882
|
+
function featureDependsOn(file) {
|
|
2883
|
+
const meterKeys = file.routes.flatMap(
|
|
2884
|
+
(route) => routeMeterDependencyKeys(route)
|
|
2885
|
+
);
|
|
2886
|
+
const backendKeys = [
|
|
2887
|
+
...file.backend !== void 0 ? [file.backend] : [],
|
|
2888
|
+
...file.routes.flatMap(
|
|
2889
|
+
(route) => route.backend !== void 0 ? [route.backend] : []
|
|
2890
|
+
)
|
|
2891
|
+
];
|
|
2892
|
+
return [
|
|
2893
|
+
...dependenciesFor("policy", file.policies),
|
|
2894
|
+
...dependenciesFor("plan", file.plans),
|
|
2895
|
+
...dependenciesFor("meter", meterKeys),
|
|
2896
|
+
...dependenciesFor("backend", backendKeys)
|
|
2897
|
+
];
|
|
2898
|
+
}
|
|
2899
|
+
function actionDependsOn(featureKey, action) {
|
|
2900
|
+
return [
|
|
2901
|
+
resourceDependency("feature", featureKey),
|
|
2902
|
+
...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
|
|
2903
|
+
];
|
|
2904
|
+
}
|
|
2905
|
+
function planDependsOn(plan, hasResource) {
|
|
2906
|
+
const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
|
|
2907
|
+
const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
|
|
2908
|
+
const pricedMeterDimensions = (plan.meters ?? []).map(
|
|
2909
|
+
(meter) => meter.dimension
|
|
2910
|
+
);
|
|
2911
|
+
const capacityKeys = Object.keys(plan.capability_limits ?? {});
|
|
2912
|
+
return [
|
|
2913
|
+
...dependenciesFor("capability", caps),
|
|
2914
|
+
...existingDependenciesFor(
|
|
2915
|
+
"meter",
|
|
2916
|
+
[...limitDimensions, ...pricedMeterDimensions],
|
|
2917
|
+
hasResource
|
|
2918
|
+
),
|
|
2919
|
+
...existingDependenciesFor("counted_resource", capacityKeys, hasResource)
|
|
2920
|
+
];
|
|
2921
|
+
}
|
|
2922
|
+
function migrationDependsOn(migration) {
|
|
2923
|
+
return [
|
|
2924
|
+
resourceDependency("plan", migration.from.plan),
|
|
2925
|
+
resourceDependency("plan", migration.to.plan),
|
|
2926
|
+
...dependenciesFor(
|
|
2927
|
+
"plan",
|
|
2928
|
+
migration.pins?.map((pin) => pin.pinTo.plan)
|
|
2929
|
+
)
|
|
2930
|
+
];
|
|
2931
|
+
}
|
|
2932
|
+
function frontendDependsOn(manifest) {
|
|
2933
|
+
return dependenciesFor("capability", frontendCapabilityKeys(manifest));
|
|
2934
|
+
}
|
|
2935
|
+
function assertResourceDependenciesSatisfied(missing) {
|
|
2936
|
+
if (missing.length === 0) return;
|
|
2937
|
+
const details = missing.slice(0, 8).map(
|
|
2938
|
+
({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
|
|
2939
|
+
dependency
|
|
2940
|
+
)}`
|
|
2941
|
+
).join("; ");
|
|
2942
|
+
const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
|
|
2943
|
+
throw new ManifestBuilderError(
|
|
2944
|
+
`manifest has unresolved resource reference(s): ${details}${suffix}`
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
function dependenciesFor(kind, keys) {
|
|
2948
|
+
return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
|
|
2949
|
+
}
|
|
2950
|
+
function existingDependenciesFor(kind, keys, hasResource) {
|
|
2951
|
+
return [...new Set(keys)].filter((key) => hasResource(kind, key)).map((key) => resourceDependency(kind, key));
|
|
2952
|
+
}
|
|
2953
|
+
function describeResourceUrn(urn) {
|
|
2954
|
+
const parts = urn.split(":");
|
|
2955
|
+
const kind = parts[3] ?? "resource";
|
|
2956
|
+
const key = parts.slice(4).join(":");
|
|
2957
|
+
return `${kind} "${decodeURIComponent(key)}"`;
|
|
2958
|
+
}
|
|
2402
2959
|
|
|
2403
|
-
// src/
|
|
2404
|
-
function
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2960
|
+
// src/declarations.ts
|
|
2961
|
+
function buildCapabilityLayer(key, options = {}) {
|
|
2962
|
+
return {
|
|
2963
|
+
capability: key,
|
|
2964
|
+
...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
|
|
2965
|
+
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2966
|
+
...options.includesFeatures?.length ? { includes_features: options.includesFeatures.map(keyOf) } : {},
|
|
2967
|
+
...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
|
|
2968
|
+
...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function buildFeatureLayer(key, options, buildRoute) {
|
|
2972
|
+
const layer = {
|
|
2973
|
+
feature: key,
|
|
2974
|
+
routes: [],
|
|
2975
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2976
|
+
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2977
|
+
...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
|
|
2978
|
+
...options.upstreamOrigin !== void 0 ? { upstream: { override_origin: options.upstreamOrigin } } : {},
|
|
2979
|
+
...options.policies?.length ? { policies: options.policies.map(keyOf) } : {},
|
|
2980
|
+
...options.plans?.length ? { plans: options.plans.map(keyOf) } : {},
|
|
2981
|
+
...options.actions?.length ? {
|
|
2982
|
+
actions: options.actions.map(({ id, ...action }) => ({
|
|
2983
|
+
id,
|
|
2984
|
+
...action
|
|
2413
2985
|
}))
|
|
2414
|
-
}
|
|
2986
|
+
} : {},
|
|
2987
|
+
...options.rolloutKey !== void 0 || options.requiredFlags?.length ? {
|
|
2988
|
+
runtime: {
|
|
2989
|
+
...options.rolloutKey !== void 0 ? { rollout_key: options.rolloutKey } : {},
|
|
2990
|
+
...options.requiredFlags?.length ? { required_flags: options.requiredFlags } : {}
|
|
2991
|
+
}
|
|
2992
|
+
} : {}
|
|
2993
|
+
};
|
|
2994
|
+
for (const route of options.routes ?? []) {
|
|
2995
|
+
layer.routes.push(buildRoute(route.match, route));
|
|
2415
2996
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
return {
|
|
2997
|
+
return layer;
|
|
2998
|
+
}
|
|
2999
|
+
function buildPolicyLayer(name, options) {
|
|
3000
|
+
return {
|
|
3001
|
+
name,
|
|
3002
|
+
type: options.type,
|
|
3003
|
+
config: options.config,
|
|
3004
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3005
|
+
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
3006
|
+
...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
|
|
3007
|
+
...options.compatibleWith ? {
|
|
3008
|
+
compatible_with: {
|
|
3009
|
+
...options.compatibleWith.routeTypes ? { route_types: options.compatibleWith.routeTypes } : {},
|
|
3010
|
+
...options.compatibleWith.meters ? { meters: options.compatibleWith.meters.map(keyOf) } : {},
|
|
3011
|
+
...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
|
|
3012
|
+
}
|
|
3013
|
+
} : {}
|
|
3014
|
+
};
|
|
2420
3015
|
}
|
|
2421
|
-
function
|
|
2422
|
-
|
|
3016
|
+
function buildSurfaceSpec(type, options = {}) {
|
|
3017
|
+
const key = options.key ?? type;
|
|
3018
|
+
return {
|
|
3019
|
+
key,
|
|
3020
|
+
type,
|
|
3021
|
+
...options.display !== void 0 ? { display: options.display } : {},
|
|
3022
|
+
...options.description !== void 0 ? { description: options.description } : {}
|
|
3023
|
+
};
|
|
2423
3024
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
)
|
|
2433
|
-
|
|
2434
|
-
|
|
3025
|
+
function buildWorkflowSpec(key, options = {}) {
|
|
3026
|
+
return {
|
|
3027
|
+
key,
|
|
3028
|
+
kind: options.kind ?? "async_job",
|
|
3029
|
+
trigger: options.trigger ?? { type: "manual" },
|
|
3030
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
3031
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3032
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
3033
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
|
|
3034
|
+
...options.estimates !== void 0 ? { estimates: options.estimates } : {},
|
|
3035
|
+
...options.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
3036
|
+
};
|
|
2435
3037
|
}
|
|
2436
|
-
function
|
|
2437
|
-
return
|
|
3038
|
+
function buildEntitlementSpec(key, options = {}) {
|
|
3039
|
+
return {
|
|
3040
|
+
key,
|
|
3041
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3042
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
3043
|
+
...options.featureGates !== void 0 ? { featureGates: options.featureGates } : {},
|
|
3044
|
+
...options.limits?.length ? { limits: options.limits } : {},
|
|
3045
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {}
|
|
3046
|
+
};
|
|
2438
3047
|
}
|
|
2439
|
-
function
|
|
2440
|
-
|
|
3048
|
+
function buildPlanSpec(key, options) {
|
|
3049
|
+
const { capabilityKeys, capabilityLimits, creditGrants } = normalizePlanGrants(options);
|
|
3050
|
+
const rawCaps = Array.isArray(options.raw?.capabilities) ? options.raw.capabilities.map(String) : [];
|
|
3051
|
+
const capabilities = [.../* @__PURE__ */ new Set([...capabilityKeys, ...rawCaps])];
|
|
3052
|
+
const spec = {
|
|
3053
|
+
key,
|
|
3054
|
+
name: options.name,
|
|
3055
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3056
|
+
...options.details ? { details: options.details } : {},
|
|
3057
|
+
...options.price ? {
|
|
3058
|
+
recurring_fee_cents: options.price.recurring_fee_cents,
|
|
3059
|
+
billing_interval: options.price.billing_interval,
|
|
3060
|
+
...options.price.free ? { free: true } : {}
|
|
3061
|
+
} : {},
|
|
3062
|
+
...options.meters ? { meters: options.meters } : {},
|
|
3063
|
+
...creditGrants.length ? { grants: creditGrants } : {},
|
|
3064
|
+
...options.trialDays !== void 0 ? { trial_days: options.trialDays } : {},
|
|
3065
|
+
...options.maxMonthlySpendCents !== void 0 ? { max_monthly_spend_cents: options.maxMonthlySpendCents } : {},
|
|
3066
|
+
...options.minMonthlySpendCents !== void 0 ? { min_monthly_spend_cents: options.minMonthlySpendCents } : {},
|
|
3067
|
+
...options.limits ? { limits: options.limits } : {},
|
|
3068
|
+
...options.featureGates ? { featureGates: options.featureGates } : {},
|
|
3069
|
+
...Object.keys(capabilityLimits).length ? { capability_limits: capabilityLimits } : {},
|
|
3070
|
+
...options.overageBehavior !== void 0 ? { overageBehavior: options.overageBehavior } : {},
|
|
3071
|
+
...options.selfServeEnabled !== void 0 ? { selfServeEnabled: options.selfServeEnabled } : {},
|
|
3072
|
+
...options.legacy !== void 0 ? { legacy: options.legacy } : {},
|
|
3073
|
+
...options.archive ? { archive: options.archive } : {},
|
|
3074
|
+
...options.raw ?? {},
|
|
3075
|
+
...capabilities.length ? { capabilities } : {}
|
|
3076
|
+
};
|
|
3077
|
+
return spec;
|
|
2441
3078
|
}
|
|
2442
|
-
function
|
|
2443
|
-
const
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2446
|
-
|
|
3079
|
+
function normalizePlanGrants(options) {
|
|
3080
|
+
const capabilityKeys = (options.capabilities ?? []).map(keyOf);
|
|
3081
|
+
const capabilityLimits = {
|
|
3082
|
+
...options.capabilityLimits ?? {}
|
|
3083
|
+
};
|
|
3084
|
+
const creditGrants = [];
|
|
3085
|
+
for (const grant of options.grants ?? []) {
|
|
3086
|
+
if (isCapabilityGrant(grant)) {
|
|
3087
|
+
if (!capabilityKeys.includes(grant.capability)) {
|
|
3088
|
+
capabilityKeys.push(grant.capability);
|
|
3089
|
+
}
|
|
3090
|
+
Object.assign(capabilityLimits, grant.limits ?? {});
|
|
3091
|
+
} else {
|
|
3092
|
+
creditGrants.push(grant);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
return { capabilityKeys, capabilityLimits, creditGrants };
|
|
3096
|
+
}
|
|
3097
|
+
function buildMigrationDecl(id, options) {
|
|
3098
|
+
return {
|
|
3099
|
+
id,
|
|
3100
|
+
from: normalizeMigrationPlanRef(options.from),
|
|
3101
|
+
to: normalizeMigrationTargetRef(options.to),
|
|
3102
|
+
newCustomers: options.newCustomers ?? "immediate",
|
|
3103
|
+
existingCustomers: options.existingCustomers,
|
|
3104
|
+
...options.pins?.length ? {
|
|
3105
|
+
pins: options.pins.map((pin) => ({
|
|
3106
|
+
subscriber: pin.subscriber,
|
|
3107
|
+
pinTo: {
|
|
3108
|
+
plan: keyOf(pin.pinTo.plan),
|
|
3109
|
+
version: pin.pinTo.version
|
|
3110
|
+
},
|
|
3111
|
+
...pin.until !== void 0 ? { until: pin.until } : {},
|
|
3112
|
+
...pin.notes !== void 0 ? { notes: pin.notes } : {}
|
|
3113
|
+
}))
|
|
3114
|
+
} : {}
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
function assertUniqueMigrationIds(migrations) {
|
|
3118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3119
|
+
for (const migration of migrations) {
|
|
3120
|
+
if (seen.has(migration.id)) {
|
|
2447
3121
|
throw new ManifestBuilderError(
|
|
2448
|
-
`
|
|
3122
|
+
`duplicate migration "${migration.id}" \u2014 each migration id must be declared once`
|
|
2449
3123
|
);
|
|
2450
3124
|
}
|
|
2451
|
-
|
|
2452
|
-
}
|
|
2453
|
-
const method = trimmed.slice(0, space).toUpperCase();
|
|
2454
|
-
const path = trimmed.slice(space + 1).trim();
|
|
2455
|
-
const methods = [
|
|
2456
|
-
"GET",
|
|
2457
|
-
"POST",
|
|
2458
|
-
"PUT",
|
|
2459
|
-
"PATCH",
|
|
2460
|
-
"DELETE",
|
|
2461
|
-
"HEAD",
|
|
2462
|
-
"OPTIONS",
|
|
2463
|
-
"*"
|
|
2464
|
-
];
|
|
2465
|
-
if (!methods.includes(method)) {
|
|
2466
|
-
throw new ManifestBuilderError(
|
|
2467
|
-
`route "${match}": unknown HTTP method "${method}"`
|
|
2468
|
-
);
|
|
2469
|
-
}
|
|
2470
|
-
if (!path.startsWith("/")) {
|
|
2471
|
-
throw new ManifestBuilderError(
|
|
2472
|
-
`route "${match}": path must start with "/"`
|
|
2473
|
-
);
|
|
3125
|
+
seen.add(migration.id);
|
|
2474
3126
|
}
|
|
2475
|
-
return { method, path };
|
|
2476
3127
|
}
|
|
3128
|
+
function normalizeMigrationPlanRef(ref) {
|
|
3129
|
+
return {
|
|
3130
|
+
plan: keyOf(ref.plan),
|
|
3131
|
+
...ref.version !== void 0 ? { version: ref.version } : {}
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
function normalizeMigrationTargetRef(ref) {
|
|
3135
|
+
return {
|
|
3136
|
+
plan: keyOf(ref.plan),
|
|
3137
|
+
version: ref.version ?? "head"
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/product.ts
|
|
3142
|
+
var PRODUCT_BRAND = Symbol.for("farthershore.product.product");
|
|
3143
|
+
var PRODUCT_MANIFEST_COMPILER = Symbol.for(
|
|
3144
|
+
"farthershore.product.manifestCompiler"
|
|
3145
|
+
);
|
|
2477
3146
|
var Product = class {
|
|
2478
3147
|
[PRODUCT_BRAND] = true;
|
|
2479
3148
|
name;
|
|
2480
3149
|
options;
|
|
2481
3150
|
graph = new ManifestResourceGraph();
|
|
2482
3151
|
defaultMeterCosts = [];
|
|
2483
|
-
frontendManifest;
|
|
2484
3152
|
productPatch = {};
|
|
2485
3153
|
/** Sugar for binding API routes to features. */
|
|
2486
3154
|
api;
|
|
2487
3155
|
frontend;
|
|
2488
3156
|
lifecycle;
|
|
2489
|
-
offering;
|
|
2490
3157
|
/** Escape hatches — raw platform-schema JSON, validated by the compiler. */
|
|
2491
3158
|
raw;
|
|
2492
3159
|
constructor(name, options) {
|
|
@@ -2507,7 +3174,7 @@ var Product = class {
|
|
|
2507
3174
|
this.api = {
|
|
2508
3175
|
route: (match, options2) => {
|
|
2509
3176
|
const featureKey = keyOf(options2.feature);
|
|
2510
|
-
const file = this.
|
|
3177
|
+
const file = this.getFeatureLayer(featureKey);
|
|
2511
3178
|
if (!file) {
|
|
2512
3179
|
throw new ManifestBuilderError(
|
|
2513
3180
|
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call product.feature("${featureKey}", \u2026) first`
|
|
@@ -2521,41 +3188,17 @@ var Product = class {
|
|
|
2521
3188
|
this.frontend = {
|
|
2522
3189
|
nav: (items) => {
|
|
2523
3190
|
const manifest = this.ensureFrontendManifest();
|
|
2524
|
-
manifest
|
|
2525
|
-
label: item.label,
|
|
2526
|
-
path: item.path,
|
|
2527
|
-
...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
|
|
2528
|
-
}));
|
|
3191
|
+
setFrontendNav(manifest, items);
|
|
2529
3192
|
this.syncFrontendGraphNode(manifest);
|
|
2530
3193
|
return this;
|
|
2531
3194
|
},
|
|
2532
3195
|
page: (path, options2) => {
|
|
2533
3196
|
const manifest = this.ensureFrontendManifest();
|
|
2534
|
-
|
|
2535
|
-
throw new ManifestBuilderError(
|
|
2536
|
-
`duplicate frontend page "${path}" \u2014 each frontend page path must be declared once`
|
|
2537
|
-
);
|
|
2538
|
-
}
|
|
2539
|
-
manifest.pages ??= [];
|
|
2540
|
-
manifest.pages.push({
|
|
2541
|
-
path,
|
|
2542
|
-
title: options2.title,
|
|
2543
|
-
requiresAuth: options2.requiresAuth,
|
|
2544
|
-
...options2.capability !== void 0 ? { capability: keyOf(options2.capability) } : {},
|
|
2545
|
-
...options2.components?.length ? {
|
|
2546
|
-
components: options2.components.map((component) => ({
|
|
2547
|
-
component: component.component,
|
|
2548
|
-
...component.props !== void 0 ? { props: component.props } : {},
|
|
2549
|
-
...component.capability !== void 0 ? { capability: keyOf(component.capability) } : {},
|
|
2550
|
-
...component.gateMode !== void 0 ? { gateMode: component.gateMode } : {}
|
|
2551
|
-
}))
|
|
2552
|
-
} : {}
|
|
2553
|
-
});
|
|
3197
|
+
addFrontendPage(manifest, path, options2);
|
|
2554
3198
|
this.syncFrontendGraphNode(manifest);
|
|
2555
3199
|
return this;
|
|
2556
3200
|
},
|
|
2557
3201
|
manifest: (manifest) => {
|
|
2558
|
-
this.frontendManifest = manifest;
|
|
2559
3202
|
this.syncFrontendGraphNode(manifest);
|
|
2560
3203
|
return this;
|
|
2561
3204
|
}
|
|
@@ -2563,77 +3206,60 @@ var Product = class {
|
|
|
2563
3206
|
this.lifecycle = {
|
|
2564
3207
|
migration: (id, options2) => {
|
|
2565
3208
|
this.assertNewKey("lifecycle_migration", id, "migration");
|
|
2566
|
-
const migration =
|
|
2567
|
-
id,
|
|
2568
|
-
from: this.normalizeMigrationPlanRef(options2.from),
|
|
2569
|
-
to: this.normalizeMigrationTargetRef(options2.to),
|
|
2570
|
-
newCustomers: options2.newCustomers ?? "immediate",
|
|
2571
|
-
existingCustomers: options2.existingCustomers,
|
|
2572
|
-
...options2.pins?.length ? {
|
|
2573
|
-
pins: options2.pins.map((pin) => ({
|
|
2574
|
-
subscriber: pin.subscriber,
|
|
2575
|
-
pinTo: {
|
|
2576
|
-
plan: keyOf(pin.pinTo.plan),
|
|
2577
|
-
version: pin.pinTo.version
|
|
2578
|
-
},
|
|
2579
|
-
...pin.until !== void 0 ? { until: pin.until } : {},
|
|
2580
|
-
...pin.notes !== void 0 ? { notes: pin.notes } : {}
|
|
2581
|
-
}))
|
|
2582
|
-
} : {}
|
|
2583
|
-
};
|
|
3209
|
+
const migration = buildMigrationDecl(id, options2);
|
|
2584
3210
|
this.graph.register(
|
|
2585
3211
|
"lifecycle_migration",
|
|
2586
3212
|
id,
|
|
2587
3213
|
migration,
|
|
2588
|
-
|
|
3214
|
+
migrationDependsOn(migration)
|
|
2589
3215
|
);
|
|
2590
3216
|
return this;
|
|
2591
3217
|
},
|
|
2592
3218
|
migrations: (migrations) => {
|
|
2593
|
-
|
|
3219
|
+
assertUniqueMigrationIds(migrations);
|
|
2594
3220
|
this.graph.clearKind("lifecycle_migration");
|
|
2595
3221
|
for (const migration of migrations) {
|
|
2596
3222
|
this.graph.register(
|
|
2597
3223
|
"lifecycle_migration",
|
|
2598
3224
|
migration.id,
|
|
2599
3225
|
migration,
|
|
2600
|
-
|
|
3226
|
+
migrationDependsOn(migration)
|
|
2601
3227
|
);
|
|
2602
3228
|
}
|
|
2603
3229
|
return this;
|
|
2604
3230
|
}
|
|
2605
3231
|
};
|
|
2606
|
-
this.offering = {
|
|
2607
|
-
plan: (key, options2) => this.plan(key, options2)
|
|
2608
|
-
};
|
|
2609
3232
|
this.raw = {
|
|
2610
3233
|
productPatch: (patch) => {
|
|
2611
|
-
this.productPatch =
|
|
3234
|
+
this.productPatch = mergeProductPatch(this.productPatch, patch);
|
|
2612
3235
|
return this;
|
|
2613
3236
|
},
|
|
2614
3237
|
plan: (spec) => {
|
|
2615
3238
|
this.assertNewKey("plan", spec.key, "plan");
|
|
2616
|
-
this.graph.register(
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
3239
|
+
this.graph.register(
|
|
3240
|
+
"plan",
|
|
3241
|
+
spec.key,
|
|
3242
|
+
spec,
|
|
3243
|
+
planDependsOn(
|
|
3244
|
+
spec,
|
|
3245
|
+
(kind, dependencyKey) => this.graph.has(kind, dependencyKey)
|
|
3246
|
+
)
|
|
3247
|
+
);
|
|
2622
3248
|
return this;
|
|
2623
3249
|
},
|
|
2624
|
-
|
|
2625
|
-
this.assertNewKey("
|
|
2626
|
-
this.
|
|
3250
|
+
routeLayer: (layer) => {
|
|
3251
|
+
this.assertNewKey("feature", layer.feature, "feature");
|
|
3252
|
+
this.registerFeatureLayer(layer);
|
|
2627
3253
|
return this;
|
|
2628
3254
|
},
|
|
2629
|
-
|
|
2630
|
-
this.assertNewKey("
|
|
2631
|
-
this.graph.register("
|
|
3255
|
+
policyLayer: (layer) => {
|
|
3256
|
+
this.assertNewKey("policy", layer.name, "policy");
|
|
3257
|
+
this.graph.register("policy", layer.name, layer);
|
|
2632
3258
|
return this;
|
|
2633
3259
|
},
|
|
2634
|
-
|
|
2635
|
-
this.
|
|
2636
|
-
this.
|
|
3260
|
+
capabilityLayer: (layer) => {
|
|
3261
|
+
this.assertNewKey("capability", layer.capability, "capability");
|
|
3262
|
+
this.graph.register("capability", layer.capability, layer);
|
|
2637
3263
|
return this;
|
|
2638
3264
|
}
|
|
2639
3265
|
};
|
|
@@ -2646,7 +3272,7 @@ var Product = class {
|
|
|
2646
3272
|
display: meterOptions.display ?? displayFromKey(key),
|
|
2647
3273
|
...meterOptions
|
|
2648
3274
|
});
|
|
2649
|
-
const ref =
|
|
3275
|
+
const ref = makeMeterRef(key);
|
|
2650
3276
|
if (routeDefault !== void 0) {
|
|
2651
3277
|
this.defaultMeterCosts.push(
|
|
2652
3278
|
this.normalizeMeterCost(ref.fixed(routeDefault))
|
|
@@ -2654,6 +3280,18 @@ var Product = class {
|
|
|
2654
3280
|
}
|
|
2655
3281
|
return ref;
|
|
2656
3282
|
}
|
|
3283
|
+
/**
|
|
3284
|
+
* BYO-Backend V1 — declare a first-class backend (an always-running HTTP
|
|
3285
|
+
* container Farther Shore wraps). Products may declare MULTIPLE backends;
|
|
3286
|
+
* routes bind to one via `route(..., { backend })`. A product with exactly
|
|
3287
|
+
* one backend (or exactly one marked `default: true`) makes it the implicit
|
|
3288
|
+
* default, so single-backend products stay zero-config.
|
|
3289
|
+
*/
|
|
3290
|
+
backend(id, options = {}) {
|
|
3291
|
+
this.assertNewKey("backend", id, "backend");
|
|
3292
|
+
this.graph.register("backend", id, createBackendNode(id, options));
|
|
3293
|
+
return { kind: "backend", key: id };
|
|
3294
|
+
}
|
|
2657
3295
|
requests(options = {}) {
|
|
2658
3296
|
return this.meter("requests", {
|
|
2659
3297
|
display: options.display ?? "Requests",
|
|
@@ -2679,20 +3317,8 @@ var Product = class {
|
|
|
2679
3317
|
}
|
|
2680
3318
|
capability(key, options = {}) {
|
|
2681
3319
|
this.assertNewKey("capability", key, "capability");
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2684
|
-
...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
|
|
2685
|
-
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2686
|
-
...options.includesFeatures?.length ? { includes_features: options.includesFeatures.map(keyOf) } : {},
|
|
2687
|
-
...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
|
|
2688
|
-
...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
|
|
2689
|
-
};
|
|
2690
|
-
this.graph.register(
|
|
2691
|
-
"capability",
|
|
2692
|
-
key,
|
|
2693
|
-
file,
|
|
2694
|
-
this.capabilityDependsOn(file)
|
|
2695
|
-
);
|
|
3320
|
+
const layer = buildCapabilityLayer(key, options);
|
|
3321
|
+
this.graph.register("capability", key, layer, capabilityDependsOn(layer));
|
|
2696
3322
|
return {
|
|
2697
3323
|
kind: "capability",
|
|
2698
3324
|
key,
|
|
@@ -2705,52 +3331,31 @@ var Product = class {
|
|
|
2705
3331
|
}
|
|
2706
3332
|
feature(key, options = {}) {
|
|
2707
3333
|
this.assertNewKey("feature", key, "feature");
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
...options.upstreamOrigin !== void 0 ? { upstream: { override_origin: options.upstreamOrigin } } : {},
|
|
2715
|
-
...options.policies?.length ? { policies: options.policies.map(keyOf) } : {},
|
|
2716
|
-
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2717
|
-
...options.plans?.length ? { plans: options.plans.map(keyOf) } : {},
|
|
2718
|
-
...options.actions?.length ? {
|
|
2719
|
-
actions: options.actions.map(({ id, ...action }) => ({
|
|
2720
|
-
id,
|
|
2721
|
-
...action
|
|
2722
|
-
}))
|
|
2723
|
-
} : {},
|
|
2724
|
-
...options.rolloutKey !== void 0 || options.requiredFlags?.length ? {
|
|
2725
|
-
runtime: {
|
|
2726
|
-
...options.rolloutKey !== void 0 ? { rollout_key: options.rolloutKey } : {},
|
|
2727
|
-
...options.requiredFlags?.length ? { required_flags: options.requiredFlags } : {}
|
|
2728
|
-
}
|
|
2729
|
-
} : {}
|
|
2730
|
-
};
|
|
2731
|
-
for (const route of options.routes ?? []) {
|
|
2732
|
-
file.routes.push(this.buildRoute(route.match, route));
|
|
2733
|
-
}
|
|
2734
|
-
this.registerFeatureFile(file);
|
|
3334
|
+
const layer = buildFeatureLayer(
|
|
3335
|
+
key,
|
|
3336
|
+
options,
|
|
3337
|
+
(match, routeOptions) => this.buildRoute(match, routeOptions)
|
|
3338
|
+
);
|
|
3339
|
+
this.registerFeatureLayer(layer);
|
|
2735
3340
|
const ref = {
|
|
2736
3341
|
kind: "feature",
|
|
2737
3342
|
key,
|
|
2738
3343
|
action: (id, actionOptions) => {
|
|
2739
|
-
|
|
2740
|
-
if (
|
|
3344
|
+
layer.actions ??= [];
|
|
3345
|
+
if (layer.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
|
|
2741
3346
|
throw new ManifestBuilderError(
|
|
2742
3347
|
`duplicate action "${id}" \u2014 each action id must be declared once`
|
|
2743
3348
|
);
|
|
2744
3349
|
}
|
|
2745
3350
|
const action = { id, ...actionOptions };
|
|
2746
|
-
|
|
3351
|
+
layer.actions.push(action);
|
|
2747
3352
|
this.registerAction(key, action);
|
|
2748
|
-
this.syncFeatureGraphNode(
|
|
3353
|
+
this.syncFeatureGraphNode(layer);
|
|
2749
3354
|
return { kind: "action", key: id };
|
|
2750
3355
|
},
|
|
2751
3356
|
route: (match, routeOptions) => {
|
|
2752
|
-
|
|
2753
|
-
this.syncFeatureGraphNode(
|
|
3357
|
+
layer.routes.push(this.buildRoute(match, routeOptions ?? {}));
|
|
3358
|
+
this.syncFeatureGraphNode(layer);
|
|
2754
3359
|
return ref;
|
|
2755
3360
|
}
|
|
2756
3361
|
};
|
|
@@ -2758,126 +3363,48 @@ var Product = class {
|
|
|
2758
3363
|
}
|
|
2759
3364
|
policy(name, options) {
|
|
2760
3365
|
this.assertNewKey("policy", name, "policy");
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
type: options.type,
|
|
2764
|
-
config: options.config,
|
|
2765
|
-
...options.description !== void 0 ? { description: options.description } : {},
|
|
2766
|
-
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2767
|
-
...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
|
|
2768
|
-
...options.compatibleWith ? {
|
|
2769
|
-
compatible_with: {
|
|
2770
|
-
...options.compatibleWith.routeTypes ? { route_types: options.compatibleWith.routeTypes } : {},
|
|
2771
|
-
...options.compatibleWith.meters ? { meters: options.compatibleWith.meters.map(keyOf) } : {},
|
|
2772
|
-
...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
|
|
2773
|
-
}
|
|
2774
|
-
} : {}
|
|
2775
|
-
};
|
|
2776
|
-
this.graph.register("policy", name, file, this.policyDependsOn(file));
|
|
3366
|
+
const layer = buildPolicyLayer(name, options);
|
|
3367
|
+
this.graph.register("policy", name, layer, policyDependsOn(layer));
|
|
2777
3368
|
return { kind: "policy", key: name };
|
|
2778
3369
|
}
|
|
2779
3370
|
surface(type, options = {}) {
|
|
2780
|
-
const
|
|
2781
|
-
this.assertNewKey("surface", key, "surface");
|
|
2782
|
-
|
|
2783
|
-
|
|
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 };
|
|
3371
|
+
const surface = buildSurfaceSpec(type, options);
|
|
3372
|
+
this.assertNewKey("surface", surface.key, "surface");
|
|
3373
|
+
this.graph.register("surface", surface.key, surface);
|
|
3374
|
+
return { kind: "surface", key: surface.key };
|
|
2790
3375
|
}
|
|
2791
3376
|
workflow(key, options = {}) {
|
|
2792
3377
|
this.assertNewKey("workflow", key, "workflow");
|
|
2793
|
-
const workflow =
|
|
2794
|
-
|
|
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
|
-
);
|
|
3378
|
+
const workflow = buildWorkflowSpec(key, options);
|
|
3379
|
+
this.graph.register("workflow", key, workflow, workflowDependsOn(workflow));
|
|
2810
3380
|
return { kind: "workflow", key };
|
|
2811
3381
|
}
|
|
2812
3382
|
entitlement(key, options = {}) {
|
|
2813
3383
|
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
|
-
};
|
|
3384
|
+
const entitlement = buildEntitlementSpec(key, options);
|
|
2822
3385
|
this.graph.register(
|
|
2823
3386
|
"entitlement",
|
|
2824
3387
|
key,
|
|
2825
3388
|
entitlement,
|
|
2826
|
-
|
|
3389
|
+
entitlementDependsOn(
|
|
3390
|
+
entitlement,
|
|
3391
|
+
(kind, dependencyKey) => this.graph.has(kind, dependencyKey)
|
|
3392
|
+
)
|
|
2827
3393
|
);
|
|
2828
3394
|
return { kind: "entitlement", key };
|
|
2829
3395
|
}
|
|
2830
3396
|
plan(key, options) {
|
|
2831
3397
|
this.assertNewKey("plan", key, "plan");
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
};
|
|
2836
|
-
const creditGrants = [];
|
|
2837
|
-
for (const grant of options.grants ?? []) {
|
|
2838
|
-
if (isCapabilityGrant(grant)) {
|
|
2839
|
-
if (!capabilityKeys.includes(grant.capability)) {
|
|
2840
|
-
capabilityKeys.push(grant.capability);
|
|
2841
|
-
}
|
|
2842
|
-
Object.assign(capabilityLimits, grant.limits ?? {});
|
|
2843
|
-
} else {
|
|
2844
|
-
creditGrants.push(grant);
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
|
-
const spec = {
|
|
3398
|
+
const spec = buildPlanSpec(key, options);
|
|
3399
|
+
this.graph.register(
|
|
3400
|
+
"plan",
|
|
2848
3401
|
key,
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
...options.price.free ? { free: true } : {}
|
|
2856
|
-
} : {},
|
|
2857
|
-
...options.meters ? { meters: options.meters } : {},
|
|
2858
|
-
...creditGrants.length ? { grants: creditGrants } : {},
|
|
2859
|
-
...options.trialDays !== void 0 ? { trial_days: options.trialDays } : {},
|
|
2860
|
-
...options.maxMonthlySpendCents !== void 0 ? { max_monthly_spend_cents: options.maxMonthlySpendCents } : {},
|
|
2861
|
-
...options.minMonthlySpendCents !== void 0 ? { min_monthly_spend_cents: options.minMonthlySpendCents } : {},
|
|
2862
|
-
...options.limits ? { limits: options.limits } : {},
|
|
2863
|
-
...options.featureGates ? { featureGates: options.featureGates } : {},
|
|
2864
|
-
...Object.keys(capabilityLimits).length ? { capability_limits: capabilityLimits } : {},
|
|
2865
|
-
...options.overageBehavior !== void 0 ? { overageBehavior: options.overageBehavior } : {},
|
|
2866
|
-
...options.selfServeEnabled !== void 0 ? { selfServeEnabled: options.selfServeEnabled } : {},
|
|
2867
|
-
...options.legacy !== void 0 ? { legacy: options.legacy } : {},
|
|
2868
|
-
...options.archive ? { archive: options.archive } : {},
|
|
2869
|
-
...options.raw ?? {}
|
|
2870
|
-
};
|
|
2871
|
-
const rawCaps = Array.isArray(
|
|
2872
|
-
spec.capabilities
|
|
2873
|
-
) ? spec.capabilities.map(
|
|
2874
|
-
String
|
|
2875
|
-
) : [];
|
|
2876
|
-
const mergedCaps = [.../* @__PURE__ */ new Set([...capabilityKeys, ...rawCaps])];
|
|
2877
|
-
if (mergedCaps.length) {
|
|
2878
|
-
spec.capabilities = mergedCaps;
|
|
2879
|
-
}
|
|
2880
|
-
this.graph.register("plan", key, spec, this.planDependsOn(spec));
|
|
3402
|
+
spec,
|
|
3403
|
+
planDependsOn(
|
|
3404
|
+
spec,
|
|
3405
|
+
(kind, dependencyKey) => this.graph.has(kind, dependencyKey)
|
|
3406
|
+
)
|
|
3407
|
+
);
|
|
2881
3408
|
return { kind: "plan", key };
|
|
2882
3409
|
}
|
|
2883
3410
|
use(...modules) {
|
|
@@ -2893,8 +3420,16 @@ var Product = class {
|
|
|
2893
3420
|
/** @internal Internal platform compiler entrypoint. Builder code exports the
|
|
2894
3421
|
* Product; the bot/CLI/build-runner decide when to compile and apply it. */
|
|
2895
3422
|
[PRODUCT_MANIFEST_COMPILER]() {
|
|
2896
|
-
const routes = this.
|
|
2897
|
-
|
|
3423
|
+
const routes = this.materializeFeatureLayers();
|
|
3424
|
+
assertRouteMeteringValid(
|
|
3425
|
+
routes,
|
|
3426
|
+
this.graph.values("meter")
|
|
3427
|
+
);
|
|
3428
|
+
assertBackendBindingsValid(
|
|
3429
|
+
routes,
|
|
3430
|
+
this.graph.values("backend"),
|
|
3431
|
+
this.graph.values("meter")
|
|
3432
|
+
);
|
|
2898
3433
|
this.assertGraphDependenciesSatisfied();
|
|
2899
3434
|
const candidate = {
|
|
2900
3435
|
irVersion: 1,
|
|
@@ -2903,245 +3438,67 @@ var Product = class {
|
|
|
2903
3438
|
routes,
|
|
2904
3439
|
policies: this.graph.sortedValues(
|
|
2905
3440
|
"policy",
|
|
2906
|
-
(
|
|
3441
|
+
(layer) => layer.name
|
|
2907
3442
|
),
|
|
2908
3443
|
capabilities: this.graph.sortedValues(
|
|
2909
3444
|
"capability",
|
|
2910
|
-
(
|
|
2911
|
-
)
|
|
2912
|
-
runtime: { rollout: null, flags: null, migrations: null }
|
|
3445
|
+
(layer) => layer.capability
|
|
3446
|
+
)
|
|
2913
3447
|
};
|
|
2914
3448
|
const result = validateManifestIr(candidate);
|
|
2915
3449
|
if (!result.ok) throw new ManifestValidationError(result.issues);
|
|
2916
3450
|
return { ir: result.ir, irHash: result.irHash };
|
|
2917
3451
|
}
|
|
2918
3452
|
buildProductSpec() {
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
meters: this.buildMeterDefinitions(),
|
|
2938
|
-
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2939
|
-
},
|
|
2940
|
-
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2941
|
-
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
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
|
-
} : {},
|
|
2961
|
-
...this.graph.has("frontend", "manifest") ? {
|
|
2962
|
-
frontend: this.graph.get(
|
|
2963
|
-
"frontend",
|
|
2964
|
-
"manifest"
|
|
2965
|
-
)?.value
|
|
2966
|
-
} : {},
|
|
2967
|
-
...this.graph.values("lifecycle_migration").length ? {
|
|
2968
|
-
migrations: this.graph.sortedValues(
|
|
2969
|
-
"lifecycle_migration",
|
|
2970
|
-
(migration) => migration.id
|
|
2971
|
-
)
|
|
2972
|
-
} : {},
|
|
2973
|
-
...this.graph.values("counted_resource").length ? {
|
|
2974
|
-
resources: this.graph.sortedValues(
|
|
2975
|
-
"counted_resource",
|
|
2976
|
-
(resource) => resource.name
|
|
2977
|
-
)
|
|
2978
|
-
} : {},
|
|
2979
|
-
plans: this.graph.sortedValues("plan", (plan) => plan.key)
|
|
2980
|
-
};
|
|
2981
|
-
return deepMerge(
|
|
2982
|
-
base,
|
|
2983
|
-
this.productPatch
|
|
2984
|
-
);
|
|
2985
|
-
}
|
|
2986
|
-
buildMeterDefinitions() {
|
|
2987
|
-
const routeValueMeters = this.routeValueMeterKeys();
|
|
2988
|
-
return this.graph.sortedValues("meter", (meter) => meter.key).map((meter) => {
|
|
2989
|
-
if (meter.aggregation !== void 0) return meter;
|
|
2990
|
-
if (!routeValueMeters.has(meter.key)) return meter;
|
|
2991
|
-
return { ...meter, aggregation: "SUM" };
|
|
3453
|
+
return assembleProductSpec({
|
|
3454
|
+
name: this.name,
|
|
3455
|
+
options: this.options,
|
|
3456
|
+
productPatch: this.productPatch,
|
|
3457
|
+
meters: this.graph.values("meter"),
|
|
3458
|
+
defaultMeterCosts: this.defaultMeterCosts,
|
|
3459
|
+
backends: this.graph.values("backend"),
|
|
3460
|
+
surfaces: this.graph.values("surface"),
|
|
3461
|
+
entitlements: this.graph.values("entitlement"),
|
|
3462
|
+
workflows: this.graph.values("workflow"),
|
|
3463
|
+
frontendManifest: this.graph.get(
|
|
3464
|
+
"frontend",
|
|
3465
|
+
"manifest"
|
|
3466
|
+
)?.value,
|
|
3467
|
+
migrations: this.graph.values("lifecycle_migration"),
|
|
3468
|
+
resources: this.graph.values("counted_resource"),
|
|
3469
|
+
plans: this.graph.values("plan"),
|
|
3470
|
+
featureLayers: this.graph.values("feature")
|
|
2992
3471
|
});
|
|
2993
3472
|
}
|
|
2994
|
-
routeValueMeterKeys() {
|
|
2995
|
-
const keys = /* @__PURE__ */ new Set();
|
|
2996
|
-
for (const cost of this.defaultMeterCosts) {
|
|
2997
|
-
keys.add(cost.meter);
|
|
2998
|
-
}
|
|
2999
|
-
for (const file of this.graph.values("feature")) {
|
|
3000
|
-
for (const route of file.routes) {
|
|
3001
|
-
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
3002
|
-
keys.add(meter);
|
|
3003
|
-
}
|
|
3004
|
-
for (const meter of route.metering?.reports ?? []) {
|
|
3005
|
-
keys.add(meter);
|
|
3006
|
-
}
|
|
3007
|
-
}
|
|
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
|
-
}
|
|
3014
|
-
return keys;
|
|
3015
|
-
}
|
|
3016
3473
|
buildRoute(match, options) {
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
...metering ? { metering } : {},
|
|
3022
|
-
...options.unmetered !== void 0 ? { unmetered: options.unmetered } : {},
|
|
3023
|
-
...options.inheritDefaultMeters !== void 0 ? { inheritDefaultMeters: options.inheritDefaultMeters } : {},
|
|
3024
|
-
...options.action !== void 0 ? { action: keyOf(options.action) } : {}
|
|
3025
|
-
};
|
|
3026
|
-
}
|
|
3027
|
-
makeMeterRef(key) {
|
|
3028
|
-
return {
|
|
3029
|
-
kind: "meter",
|
|
3030
|
-
key,
|
|
3031
|
-
fixed: (value) => ({ kind: "meter_cost", meter: key, value }),
|
|
3032
|
-
estimate: (value) => ({ kind: "meter_cost", meter: key, value })
|
|
3033
|
-
};
|
|
3474
|
+
return buildRouteDefinition(match, options, {
|
|
3475
|
+
normalizeMeterCost: (cost) => this.normalizeMeterCost(cost),
|
|
3476
|
+
getMeterDefinition: (key) => this.graph.get("meter", key)?.value
|
|
3477
|
+
});
|
|
3034
3478
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const reports = [
|
|
3042
|
-
...new Set(this.normalizeMeterRefs(options.reports ?? []))
|
|
3043
|
-
];
|
|
3044
|
-
const estimates = {};
|
|
3045
|
-
for (const meter of reports) {
|
|
3046
|
-
const definition = this.graph.get(
|
|
3047
|
-
"meter",
|
|
3048
|
-
meter
|
|
3049
|
-
)?.value;
|
|
3050
|
-
if (typeof definition?.estimate === "number") {
|
|
3051
|
-
estimates[meter] = definition.estimate;
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
for (const [meter, value] of Object.entries(options.estimates ?? {})) {
|
|
3055
|
-
estimates[meter] = value;
|
|
3056
|
-
}
|
|
3057
|
-
const out = {};
|
|
3058
|
-
if (Object.keys(defaults).length) out.defaults = defaults;
|
|
3059
|
-
if (reports.length) out.reports = reports;
|
|
3060
|
-
if (Object.keys(estimates).length) out.estimates = estimates;
|
|
3061
|
-
if (options.onStatusCodes !== void 0)
|
|
3062
|
-
out.onStatusCodes = options.onStatusCodes;
|
|
3063
|
-
return Object.keys(out).length ? out : void 0;
|
|
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))
|
|
3479
|
+
materializeFeatureLayers() {
|
|
3480
|
+
return this.graph.sortedValues("feature", (layer) => layer.feature).map((layer) => ({
|
|
3481
|
+
...layer,
|
|
3482
|
+
routes: layer.routes.map(
|
|
3483
|
+
(route) => materializeRoute(route, this.defaultMeterCosts)
|
|
3484
|
+
)
|
|
3069
3485
|
}));
|
|
3070
3486
|
}
|
|
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
|
-
}
|
|
3091
3487
|
normalizeMeterCost(cost) {
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
}
|
|
3097
|
-
if (!this.graph.has("meter", cost.meter)) {
|
|
3098
|
-
throw new ManifestBuilderError(
|
|
3099
|
-
`meter cost references unknown meter "${cost.meter}"`
|
|
3100
|
-
);
|
|
3101
|
-
}
|
|
3102
|
-
if (!Number.isFinite(cost.value) || cost.value < 0) {
|
|
3103
|
-
throw new ManifestBuilderError(
|
|
3104
|
-
`meter "${cost.meter}" fixed value must be a non-negative finite number`
|
|
3105
|
-
);
|
|
3106
|
-
}
|
|
3107
|
-
return cost;
|
|
3108
|
-
}
|
|
3109
|
-
normalizeMeterCosts(costs) {
|
|
3110
|
-
if (!costs) return [];
|
|
3111
|
-
const entries = Array.isArray(costs) ? costs : [costs];
|
|
3112
|
-
return entries.map((cost) => this.normalizeMeterCost(cost));
|
|
3113
|
-
}
|
|
3114
|
-
normalizeMeterRefs(refs) {
|
|
3115
|
-
const entries = Array.isArray(refs) ? refs : [refs];
|
|
3116
|
-
return entries.map(keyOf);
|
|
3488
|
+
return normalizeMeterCost(
|
|
3489
|
+
cost,
|
|
3490
|
+
(meter) => this.graph.has("meter", meter)
|
|
3491
|
+
);
|
|
3117
3492
|
}
|
|
3118
3493
|
ensureFrontendManifest() {
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
};
|
|
3128
|
-
}
|
|
3129
|
-
normalizeMigrationTargetRef(ref) {
|
|
3130
|
-
return {
|
|
3131
|
-
plan: keyOf(ref.plan),
|
|
3132
|
-
version: ref.version ?? "head"
|
|
3133
|
-
};
|
|
3134
|
-
}
|
|
3135
|
-
assertUniqueMigrationIds(migrations) {
|
|
3136
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3137
|
-
for (const migration of migrations) {
|
|
3138
|
-
if (seen.has(migration.id)) {
|
|
3139
|
-
throw new ManifestBuilderError(
|
|
3140
|
-
`duplicate migration "${migration.id}" \u2014 each migration id must be declared once`
|
|
3141
|
-
);
|
|
3142
|
-
}
|
|
3143
|
-
seen.add(migration.id);
|
|
3144
|
-
}
|
|
3494
|
+
const existing = this.graph.get(
|
|
3495
|
+
"frontend",
|
|
3496
|
+
"manifest"
|
|
3497
|
+
)?.value;
|
|
3498
|
+
if (existing) return existing;
|
|
3499
|
+
const manifest = createFrontendManifest();
|
|
3500
|
+
this.syncFrontendGraphNode(manifest);
|
|
3501
|
+
return manifest;
|
|
3145
3502
|
}
|
|
3146
3503
|
assertNewKey(kind, key, label) {
|
|
3147
3504
|
if (this.graph.has(kind, key)) {
|
|
@@ -3151,39 +3508,24 @@ var Product = class {
|
|
|
3151
3508
|
}
|
|
3152
3509
|
}
|
|
3153
3510
|
assertGraphDependenciesSatisfied() {
|
|
3154
|
-
|
|
3155
|
-
if (missing.length === 0) return;
|
|
3156
|
-
const details = missing.slice(0, 8).map(
|
|
3157
|
-
({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
|
|
3158
|
-
dependency
|
|
3159
|
-
)}`
|
|
3160
|
-
).join("; ");
|
|
3161
|
-
const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
|
|
3162
|
-
throw new ManifestBuilderError(
|
|
3163
|
-
`manifest has unresolved resource reference(s): ${details}${suffix}`
|
|
3164
|
-
);
|
|
3511
|
+
assertResourceDependenciesSatisfied(this.graph.missingDependencies());
|
|
3165
3512
|
}
|
|
3166
|
-
|
|
3513
|
+
getFeatureLayer(key) {
|
|
3167
3514
|
return this.graph.get("feature", key)?.value ?? null;
|
|
3168
3515
|
}
|
|
3169
|
-
|
|
3516
|
+
registerFeatureLayer(layer) {
|
|
3170
3517
|
this.graph.register(
|
|
3171
3518
|
"feature",
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3519
|
+
layer.feature,
|
|
3520
|
+
layer,
|
|
3521
|
+
featureDependsOn(layer)
|
|
3175
3522
|
);
|
|
3176
|
-
for (const action of
|
|
3177
|
-
this.registerAction(
|
|
3523
|
+
for (const action of layer.actions ?? []) {
|
|
3524
|
+
this.registerAction(layer.feature, action);
|
|
3178
3525
|
}
|
|
3179
3526
|
}
|
|
3180
|
-
syncFeatureGraphNode(
|
|
3181
|
-
this.graph.upsert(
|
|
3182
|
-
"feature",
|
|
3183
|
-
file.feature,
|
|
3184
|
-
file,
|
|
3185
|
-
this.featureDependsOn(file)
|
|
3186
|
-
);
|
|
3527
|
+
syncFeatureGraphNode(layer) {
|
|
3528
|
+
this.graph.upsert("feature", layer.feature, layer, featureDependsOn(layer));
|
|
3187
3529
|
}
|
|
3188
3530
|
registerAction(featureKey, action) {
|
|
3189
3531
|
this.assertNewKey("action", action.id, "action");
|
|
@@ -3191,7 +3533,7 @@ var Product = class {
|
|
|
3191
3533
|
"action",
|
|
3192
3534
|
action.id,
|
|
3193
3535
|
action,
|
|
3194
|
-
|
|
3536
|
+
actionDependsOn(featureKey, action)
|
|
3195
3537
|
);
|
|
3196
3538
|
}
|
|
3197
3539
|
syncFrontendGraphNode(manifest) {
|
|
@@ -3199,163 +3541,8 @@ var Product = class {
|
|
|
3199
3541
|
"frontend",
|
|
3200
3542
|
"manifest",
|
|
3201
3543
|
manifest,
|
|
3202
|
-
|
|
3203
|
-
);
|
|
3204
|
-
}
|
|
3205
|
-
capabilityDependsOn(file) {
|
|
3206
|
-
return [
|
|
3207
|
-
...this.dependenciesFor("feature", file.includes_features),
|
|
3208
|
-
...this.dependenciesFor("policy", file.includes_policies),
|
|
3209
|
-
...this.dependenciesFor("capability", file.includes_capabilities)
|
|
3210
|
-
];
|
|
3211
|
-
}
|
|
3212
|
-
policyDependsOn(file) {
|
|
3213
|
-
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
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
|
-
}
|
|
3236
|
-
featureDependsOn(file) {
|
|
3237
|
-
const meterKeys = file.routes.flatMap(
|
|
3238
|
-
(route) => this.routeMeterDependencyKeys(route)
|
|
3239
|
-
);
|
|
3240
|
-
return [
|
|
3241
|
-
...this.dependenciesFor("policy", file.policies),
|
|
3242
|
-
...this.dependenciesFor("capability", file.capabilities),
|
|
3243
|
-
...this.dependenciesFor("plan", file.plans),
|
|
3244
|
-
...this.dependenciesFor("meter", meterKeys)
|
|
3245
|
-
];
|
|
3246
|
-
}
|
|
3247
|
-
routeMeterDependencyKeys(route) {
|
|
3248
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3249
|
-
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
3250
|
-
keys.add(meter);
|
|
3251
|
-
}
|
|
3252
|
-
for (const meter of route.metering?.reports ?? []) keys.add(meter);
|
|
3253
|
-
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
3254
|
-
keys.add(meter);
|
|
3255
|
-
}
|
|
3256
|
-
return [...keys];
|
|
3257
|
-
}
|
|
3258
|
-
assertRouteMeteringValid(files) {
|
|
3259
|
-
const declaredMeters = new Set(
|
|
3260
|
-
this.graph.values("meter").map((meter) => meter.key)
|
|
3261
|
-
);
|
|
3262
|
-
for (const file of files) {
|
|
3263
|
-
file.routes.forEach((route, routeIndex) => {
|
|
3264
|
-
if (route.unmetered === true) return;
|
|
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
|
-
}
|
|
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
|
-
}
|
|
3278
|
-
if (defaults.has(meter)) {
|
|
3279
|
-
throw new ManifestBuilderError(
|
|
3280
|
-
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
3281
|
-
);
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
for (const meter of route.metering?.reports ?? []) {
|
|
3285
|
-
const definition = this.graph.get(
|
|
3286
|
-
"meter",
|
|
3287
|
-
meter
|
|
3288
|
-
)?.value;
|
|
3289
|
-
const enforcement = definition?.enforcementType ?? "estimated_then_settled";
|
|
3290
|
-
const hasEstimate = route.metering?.estimates && Object.prototype.hasOwnProperty.call(
|
|
3291
|
-
route.metering.estimates,
|
|
3292
|
-
meter
|
|
3293
|
-
);
|
|
3294
|
-
if ((enforcement === "exact_pre_request" || enforcement === "estimated_then_settled") && !hasEstimate) {
|
|
3295
|
-
throw new ManifestBuilderError(
|
|
3296
|
-
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" needs an estimate for gateway admission`
|
|
3297
|
-
);
|
|
3298
|
-
}
|
|
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
|
-
}
|
|
3306
|
-
});
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
actionDependsOn(featureKey, action) {
|
|
3310
|
-
return [
|
|
3311
|
-
resourceDependency("feature", featureKey),
|
|
3312
|
-
...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
|
|
3313
|
-
];
|
|
3314
|
-
}
|
|
3315
|
-
planDependsOn(plan) {
|
|
3316
|
-
const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
|
|
3317
|
-
const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
|
|
3318
|
-
const pricedMeterDimensions = (plan.meters ?? []).map(
|
|
3319
|
-
(meter) => meter.dimension
|
|
3544
|
+
frontendDependsOn(manifest)
|
|
3320
3545
|
);
|
|
3321
|
-
const capacityKeys = Object.keys(plan.capability_limits ?? {});
|
|
3322
|
-
return [
|
|
3323
|
-
...this.dependenciesFor("capability", caps),
|
|
3324
|
-
...this.existingDependenciesFor("meter", [
|
|
3325
|
-
...limitDimensions,
|
|
3326
|
-
...pricedMeterDimensions
|
|
3327
|
-
]),
|
|
3328
|
-
...this.existingDependenciesFor("counted_resource", capacityKeys)
|
|
3329
|
-
];
|
|
3330
|
-
}
|
|
3331
|
-
migrationDependsOn(migration) {
|
|
3332
|
-
return [
|
|
3333
|
-
resourceDependency("plan", migration.from.plan),
|
|
3334
|
-
resourceDependency("plan", migration.to.plan),
|
|
3335
|
-
...this.dependenciesFor(
|
|
3336
|
-
"plan",
|
|
3337
|
-
migration.pins?.map((pin) => pin.pinTo.plan)
|
|
3338
|
-
)
|
|
3339
|
-
];
|
|
3340
|
-
}
|
|
3341
|
-
frontendDependsOn(manifest) {
|
|
3342
|
-
return this.dependenciesFor("capability", [
|
|
3343
|
-
...(manifest.nav ?? []).flatMap(
|
|
3344
|
-
(item) => item.capability ? [item.capability] : []
|
|
3345
|
-
),
|
|
3346
|
-
...(manifest.pages ?? []).flatMap((page) => [
|
|
3347
|
-
...page.capability ? [page.capability] : [],
|
|
3348
|
-
...(page.components ?? []).flatMap(
|
|
3349
|
-
(component) => component.capability ? [component.capability] : []
|
|
3350
|
-
)
|
|
3351
|
-
])
|
|
3352
|
-
]);
|
|
3353
|
-
}
|
|
3354
|
-
dependenciesFor(kind, keys) {
|
|
3355
|
-
return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
|
|
3356
|
-
}
|
|
3357
|
-
existingDependenciesFor(kind, keys) {
|
|
3358
|
-
return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
|
|
3359
3546
|
}
|
|
3360
3547
|
};
|
|
3361
3548
|
function isProduct(value) {
|
|
@@ -3364,34 +3551,6 @@ function isProduct(value) {
|
|
|
3364
3551
|
function compileProductToManifest(product) {
|
|
3365
3552
|
return product[PRODUCT_MANIFEST_COMPILER]();
|
|
3366
3553
|
}
|
|
3367
|
-
function isPlainObject(value) {
|
|
3368
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3369
|
-
}
|
|
3370
|
-
function buildCustomerContext(options) {
|
|
3371
|
-
return {
|
|
3372
|
-
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
3373
|
-
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
3374
|
-
...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
|
|
3375
|
-
};
|
|
3376
|
-
}
|
|
3377
|
-
function deepMerge(base, patch) {
|
|
3378
|
-
const out = { ...base };
|
|
3379
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
3380
|
-
const existing = out[key];
|
|
3381
|
-
if (isPlainObject(existing) && isPlainObject(value)) {
|
|
3382
|
-
out[key] = deepMerge(existing, value);
|
|
3383
|
-
} else {
|
|
3384
|
-
out[key] = value;
|
|
3385
|
-
}
|
|
3386
|
-
}
|
|
3387
|
-
return out;
|
|
3388
|
-
}
|
|
3389
|
-
function describeResourceUrn(urn) {
|
|
3390
|
-
const parts = urn.split(":");
|
|
3391
|
-
const kind = parts[3] ?? "resource";
|
|
3392
|
-
const key = parts.slice(4).join(":");
|
|
3393
|
-
return `${kind} "${decodeURIComponent(key)}"`;
|
|
3394
|
-
}
|
|
3395
3554
|
|
|
3396
3555
|
// src/bin.ts
|
|
3397
3556
|
var DEFAULT_ENTRY_CANDIDATES = [
|