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