@sequenceholdings/managed-functions 0.2.0 → 0.3.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/dist/orm.js CHANGED
@@ -21,15 +21,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var orm_exports = {};
22
22
  __export(orm_exports, {
23
23
  FunctionOrmError: () => FunctionOrmError,
24
+ ORM_JSON_SCALARS_HEADER: () => ORM_JSON_SCALARS_HEADER,
25
+ ORM_LOSSLESS_JSON_SCALARS: () => ORM_LOSSLESS_JSON_SCALARS,
24
26
  OrmClientError: () => OrmClientError,
25
27
  OrmOperationError: () => OrmOperationError,
26
- createFunctionOrmExecutor: () => createFunctionOrmExecutor
28
+ createFunctionOrmExecutor: () => createFunctionOrmExecutor,
29
+ createFunctionServiceOrmExecutor: () => createFunctionServiceOrmExecutor
27
30
  });
28
31
  module.exports = __toCommonJS(orm_exports);
29
32
 
30
33
  // ../orm/dist/client.js
31
34
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
32
35
  var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
36
+ var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
33
37
  var MATERIALIZE_ACTOR = "orm:materialize";
34
38
  JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
35
39
  var OrmClientError = class extends Error {
@@ -78,6 +82,11 @@ function readOperationResponse({
78
82
  }
79
83
  return payload.data;
80
84
  }
85
+ function validateIdempotencyKey(key) {
86
+ if (key !== void 0 && !/^[A-Za-z0-9._:-]{1,128}$/.test(key)) {
87
+ throw new OrmClientError("idempotencyKey must be 1\u2013128 letters, digits, dots, underscores, colons or hyphens");
88
+ }
89
+ }
81
90
  var operationHash = /^sha256:[a-f0-9]{64}$/;
82
91
  function readManifestHashes(manifest) {
83
92
  if (!isRecord(manifest) || typeof manifest.schemaHash !== "string" || !manifest.schemaHash || !isRecord(manifest.operations)) {
@@ -96,18 +105,25 @@ function createOperationExecutorWithTransport({ namespace, manifest, transport }
96
105
  if (typeof namespace !== "string" || !NAMESPACE_PATTERN.test(namespace)) throw new OrmClientError("invalid ORM namespace \u2014 expected a lowercase identifier of at most 41 characters");
97
106
  const hashes = readManifestHashes(manifest);
98
107
  const knownHashes = new Set(hashes.values());
99
- return {
100
- async execute(op, variables = {}) {
101
- const hash = hashes.get(op) ?? (knownHashes.has(op) ? op : void 0);
102
- if (!hash) throw new OrmClientError(`unknown operation "${op}" \u2014 not in the manifest (${hashes.size} known). Re-run seq-studio orm generate.`);
103
- if (!isRecord(variables)) throw new OrmClientError("operation variables must be an object");
104
- const response = await transport({ namespace, op: hash, variables });
105
- if (!response || typeof response.ok !== "boolean" || !Number.isInteger(response.status) || response.status < 100 || response.status > 599 || response.ok !== (response.status >= 200 && response.status < 300)) {
106
- throw new OrmOperationError("malformed transport response", null, null);
107
- }
108
- return readOperationResponse(response);
108
+ async function execute(op, variables = {}, options = {}) {
109
+ validateIdempotencyKey(options.idempotencyKey);
110
+ if (typeof op !== "string" && op.namespace !== namespace) throw new OrmClientError("operation belongs to a different namespace");
111
+ const selector = typeof op === "string" ? op : op.hash;
112
+ const hash = hashes.get(selector) ?? (knownHashes.has(selector) ? selector : void 0);
113
+ if (!hash) throw new OrmClientError(`unknown operation "${op}" \u2014 not in the manifest (${hashes.size} known). Re-run seq-studio orm generate.`);
114
+ if (!isRecord(variables)) throw new OrmClientError("operation variables must be an object");
115
+ const response = await transport({
116
+ namespace,
117
+ op: hash,
118
+ variables,
119
+ ...options.idempotencyKey === void 0 ? {} : { idempotencyKey: options.idempotencyKey }
120
+ });
121
+ if (!response || typeof response.ok !== "boolean" || !Number.isInteger(response.status) || response.status < 100 || response.status > 599 || response.ok !== (response.status >= 200 && response.status < 300)) {
122
+ throw new OrmOperationError("malformed transport response", null, null);
109
123
  }
110
- };
124
+ return readOperationResponse(response);
125
+ }
126
+ return { execute };
111
127
  }
112
128
 
113
129
  // src/orm.ts
@@ -118,21 +134,20 @@ var FunctionOrmError = class extends Error {
118
134
  }
119
135
  name = "FunctionOrmError";
120
136
  };
121
- function readToken({ request }) {
137
+ var ORM_JSON_SCALARS_HEADER = "x-sequence-orm-json-scalars";
138
+ var ORM_LOSSLESS_JSON_SCALARS = "lossless-v1";
139
+ function readToken({ request, header, missing }) {
122
140
  const { headers } = request;
123
141
  let value;
124
142
  if (typeof headers.get === "function") {
125
- value = headers.get(ORM_DATA_TOKEN_HEADER);
143
+ value = headers.get(header);
126
144
  } else {
127
- const matches = Object.entries(headers).filter(([name]) => name.toLowerCase() === ORM_DATA_TOKEN_HEADER);
145
+ const matches = Object.entries(headers).filter(([name]) => name.toLowerCase() === header);
128
146
  if (matches.length > 1) throw invalidContext();
129
147
  value = matches[0]?.[1];
130
148
  }
131
149
  if (value === void 0 || value === null || value === "") {
132
- throw new FunctionOrmError(
133
- "MISSING_CALLER_CONTEXT",
134
- "Caller ORM access is unavailable. Declare the function data binding and invoke it through the platform as a signed-in user."
135
- );
150
+ throw new FunctionOrmError("MISSING_CALLER_CONTEXT", missing);
136
151
  }
137
152
  if (typeof value !== "string" || value.length > 32768) throw invalidContext();
138
153
  return value;
@@ -143,7 +158,7 @@ function invalidContext() {
143
158
  function record(value) {
144
159
  return value !== null && typeof value === "object" && !Array.isArray(value);
145
160
  }
146
- function callerExpiry({ token }) {
161
+ function invocationClaims({ token }) {
147
162
  const parts = /^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]{43})$/.exec(token);
148
163
  if (!parts?.[1]) throw invalidContext();
149
164
  let claims;
@@ -153,10 +168,24 @@ function callerExpiry({ token }) {
153
168
  throw invalidContext();
154
169
  }
155
170
  if (!record(claims) || typeof claims.sub !== "string" || !claims.sub || typeof claims.functionId !== "string" || !claims.functionId || typeof claims.invocationId !== "string" || !claims.invocationId || typeof claims.exp !== "number" || !Number.isSafeInteger(claims.exp) || claims.origin !== void 0 && claims.origin !== "function") throw invalidContext();
171
+ return { sub: claims.sub, exp: claims.exp, assertedBy: claims.assertedBy };
172
+ }
173
+ function callerExpiry({ token }) {
174
+ const claims = invocationClaims({ token });
156
175
  if (claims.sub.startsWith("service-account|") || claims.sub.endsWith("@clients") || claims.assertedBy != null) {
157
176
  throw new FunctionOrmError(
158
177
  "UNSUPPORTED_CALLER",
159
- "This ORM helper requires a direct human invocation. Service-account and system-runner invocations are not supported."
178
+ "This ORM helper requires a direct human invocation. For system-runner (Lattice) invocations, use createFunctionServiceOrmExecutor to act as the function's attached service account."
179
+ );
180
+ }
181
+ return claims.exp;
182
+ }
183
+ function serviceExpiry({ token }) {
184
+ const claims = invocationClaims({ token });
185
+ if (!claims.sub.startsWith("service-account|") || typeof claims.assertedBy !== "string" || !claims.assertedBy) {
186
+ throw new FunctionOrmError(
187
+ "UNSUPPORTED_CALLER",
188
+ "The service data token does not name the function's attached service account."
160
189
  );
161
190
  }
162
191
  return claims.exp;
@@ -185,40 +214,84 @@ function trustedOrigin({ baseUrl }) {
185
214
  }
186
215
  return url.origin;
187
216
  }
188
- function createFunctionOrmExecutor({
189
- request,
217
+ function createExecutor({
190
218
  baseUrl,
191
219
  namespace,
192
220
  manifest,
193
- fetchImpl = globalThis.fetch
221
+ fetchImpl,
222
+ token,
223
+ header,
224
+ expiresAt,
225
+ jsonScalars
194
226
  }) {
195
227
  const origin = trustedOrigin({ baseUrl });
196
- const token = readToken({ request });
197
- const expiresAt = callerExpiry({ token });
198
228
  requireUnexpired({ expiresAt });
199
229
  if (typeof fetchImpl !== "function") {
200
- throw new FunctionOrmError("INVALID_CONFIGURATION", "Caller ORM access requires a fetch implementation.");
230
+ throw new FunctionOrmError("INVALID_CONFIGURATION", "Function ORM access requires a fetch implementation.");
231
+ }
232
+ if (jsonScalars !== void 0 && jsonScalars !== ORM_LOSSLESS_JSON_SCALARS) {
233
+ throw new FunctionOrmError("INVALID_CONFIGURATION", `jsonScalars must be ${ORM_LOSSLESS_JSON_SCALARS} when supplied.`);
201
234
  }
202
235
  return createOperationExecutorWithTransport({
203
236
  namespace,
204
237
  manifest,
205
- transport: async ({ namespace: targetNamespace, op, variables }) => {
238
+ transport: async ({ namespace: targetNamespace, op, variables, idempotencyKey }) => {
206
239
  requireUnexpired({ expiresAt });
207
240
  const response = await fetchImpl(`${origin}/_data/v2/${targetNamespace}`, {
208
241
  method: "POST",
209
- headers: { "Content-Type": "application/json", [ORM_DATA_TOKEN_HEADER]: token },
210
- body: JSON.stringify({ op, variables }),
242
+ headers: {
243
+ "Content-Type": "application/json",
244
+ [header]: token,
245
+ ...jsonScalars ? { [ORM_JSON_SCALARS_HEADER]: jsonScalars } : {}
246
+ },
247
+ body: JSON.stringify({ op, variables, idempotencyKey }),
211
248
  redirect: "error"
212
249
  });
213
250
  const body = await response.json().catch(() => null);
251
+ if (jsonScalars && response.ok && response.headers.get(ORM_JSON_SCALARS_HEADER) !== jsonScalars) {
252
+ throw new OrmOperationError(
253
+ `Atlas did not confirm ${jsonScalars} numbers for this ORM response, so its numeric and bigint values may be rounded; update Atlas before relying on exact numbers`,
254
+ response.status,
255
+ "LOSSLESS_TRANSPORT_UNAVAILABLE"
256
+ );
257
+ }
214
258
  return { ok: response.ok, status: response.status, body };
215
259
  }
216
260
  });
217
261
  }
262
+ function createFunctionOrmExecutor({
263
+ request,
264
+ fetchImpl = globalThis.fetch,
265
+ ...options
266
+ }) {
267
+ trustedOrigin({ baseUrl: options.baseUrl });
268
+ const token = readToken({
269
+ request,
270
+ header: ORM_DATA_TOKEN_HEADER,
271
+ missing: "Caller ORM access is unavailable. Declare the function data binding and invoke it through the platform as a signed-in user."
272
+ });
273
+ return createExecutor({ ...options, fetchImpl, token, header: ORM_DATA_TOKEN_HEADER, expiresAt: callerExpiry({ token }) });
274
+ }
275
+ function createFunctionServiceOrmExecutor({
276
+ request,
277
+ fetchImpl = globalThis.fetch,
278
+ ...options
279
+ }) {
280
+ trustedOrigin({ baseUrl: options.baseUrl });
281
+ const token = readToken({
282
+ request,
283
+ header: ORM_SERVICE_DATA_TOKEN_HEADER,
284
+ missing: "Service ORM access is unavailable. Declare service_account and the data binding in managed-function.yml and invoke the function through the platform."
285
+ });
286
+ return createExecutor({ ...options, fetchImpl, token, header: ORM_SERVICE_DATA_TOKEN_HEADER, expiresAt: serviceExpiry({ token }) });
287
+ }
218
288
  // Annotate the CommonJS export names for ESM import in node:
219
289
  0 && (module.exports = {
220
290
  FunctionOrmError,
291
+ ORM_JSON_SCALARS_HEADER,
292
+ ORM_LOSSLESS_JSON_SCALARS,
221
293
  OrmClientError,
222
294
  OrmOperationError,
223
- createFunctionOrmExecutor
295
+ createFunctionOrmExecutor,
296
+ createFunctionServiceOrmExecutor
224
297
  });
@@ -0,0 +1,9 @@
1
+ import type { ManagedFunctionLimitCatalog } from './limits';
2
+ import { type ManagedFunctionCapabilities, type PreflightIssue, type PreflightManifest } from './capabilities';
3
+ export declare function preflightManagedFunction({ manifest, limits, capabilities, cliVersion, sdkVersion, }: {
4
+ manifest: PreflightManifest;
5
+ limits: ManagedFunctionLimitCatalog | null;
6
+ capabilities: ManagedFunctionCapabilities | null;
7
+ cliVersion: string;
8
+ sdkVersion: string | null;
9
+ }): PreflightIssue[];
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preflightManagedFunction = preflightManagedFunction;
4
+ const capabilities_1 = require("./capabilities");
5
+ const sdk_range_1 = require("./sdk-range");
6
+ const WORKLOAD_FIELDS = [
7
+ 'memory_mb',
8
+ 'timeout_seconds',
9
+ 'min_instances',
10
+ 'max_instances',
11
+ 'invoke_rate_per_minute',
12
+ 'max_response_bytes',
13
+ 'concurrency',
14
+ ];
15
+ function preflightManagedFunction({ manifest, limits, capabilities, cliVersion, sdkVersion, }) {
16
+ const tooling = capabilities?.tooling ?? capabilities_1.MANAGED_FUNCTION_CAPABILITIES.tooling;
17
+ const issues = [];
18
+ checkWorkload({ manifest, limits, tooling, cliVersion, sdkVersion, issues });
19
+ checkPostgres({ manifest, capabilities, tooling, cliVersion, sdkVersion, issues });
20
+ checkHeaders({ manifest, capabilities, tooling, cliVersion, sdkVersion, issues });
21
+ checkSdkTimeout({ manifest, limits, capabilities, tooling, cliVersion, sdkVersion, issues });
22
+ return issues;
23
+ }
24
+ function issue({ field, problem, limit, tooling, requiredCli, requiredSdk, cliVersion, sdkVersion, }) {
25
+ const installedSdk = sdkVersion ?? 'not declared';
26
+ return {
27
+ field,
28
+ limit,
29
+ requiredCli,
30
+ requiredSdk,
31
+ message: `${field}: ${problem} Supported limit: ${limit}. ` +
32
+ `Requires ${tooling.cli.package} >= ${requiredCli} (installed ${cliVersion}) ` +
33
+ `and ${tooling.sdk.package} >= ${requiredSdk} (installed ${installedSdk}).`,
34
+ };
35
+ }
36
+ function numericCap(entry) {
37
+ if (!entry || typeof entry !== 'object')
38
+ return null;
39
+ const min = Reflect.get(entry, 'min');
40
+ const max = Reflect.get(entry, 'max');
41
+ const unit = Reflect.get(entry, 'unit');
42
+ if (typeof min !== 'number' || typeof max !== 'number')
43
+ return null;
44
+ return { min, max, unit: typeof unit === 'string' ? unit : undefined };
45
+ }
46
+ function checkWorkload({ manifest, limits, tooling, cliVersion, sdkVersion, issues, }) {
47
+ const workload = limits?.workload;
48
+ if (!workload)
49
+ return;
50
+ const declared = manifest.limits;
51
+ if (!declared)
52
+ return;
53
+ for (const name of WORKLOAD_FIELDS) {
54
+ const value = declared[name];
55
+ if (typeof value !== 'number')
56
+ continue;
57
+ const cap = numericCap(workload[name]);
58
+ if (!cap)
59
+ continue;
60
+ if (Number.isInteger(value) && value >= cap.min && value <= cap.max)
61
+ continue;
62
+ const unit = cap.unit ? ` ${cap.unit}` : '';
63
+ issues.push(issue({
64
+ field: `limits.${name}`,
65
+ problem: `${value} is outside the range this deployment enforces`,
66
+ limit: `${cap.min} to ${cap.max}${unit}`,
67
+ tooling,
68
+ requiredCli: tooling.cli.min,
69
+ requiredSdk: tooling.sdk.min,
70
+ cliVersion,
71
+ sdkVersion,
72
+ }));
73
+ }
74
+ const cpu = declared.cpu;
75
+ const cpuCap = numericCap(workload.cpu);
76
+ if (cpu === undefined || cpu === 'auto' || !cpuCap)
77
+ return;
78
+ if (typeof cpu === 'number' && Number.isInteger(cpu) && cpu >= cpuCap.min && cpu <= cpuCap.max)
79
+ return;
80
+ issues.push(issue({
81
+ field: 'limits.cpu',
82
+ problem: `${String(cpu)} is outside the range this deployment enforces`,
83
+ limit: `${cpuCap.min} to ${cpuCap.max}, or auto`,
84
+ tooling,
85
+ requiredCli: tooling.cli.min,
86
+ requiredSdk: tooling.sdk.min,
87
+ cliVersion,
88
+ sdkVersion,
89
+ }));
90
+ }
91
+ function checkPostgres({ manifest, capabilities, tooling, cliVersion, sdkVersion, issues, }) {
92
+ const feature = capabilities?.features.postgres_egress;
93
+ if (!feature)
94
+ return;
95
+ const entries = manifest.egress ?? [];
96
+ entries.forEach((entry, index) => {
97
+ if (!(0, capabilities_1.isPostgresEgress)(entry))
98
+ return;
99
+ const field = `egress[${index}]`;
100
+ if (!feature.supported) {
101
+ issues.push(issue({
102
+ field,
103
+ problem: 'PostgreSQL egress is not supported by this deployment',
104
+ limit: 'hostnames, http(s) URLs, and public IPv4 CIDRs',
105
+ tooling,
106
+ requiredCli: feature.min_cli,
107
+ requiredSdk: feature.min_sdk,
108
+ cliVersion,
109
+ sdkVersion,
110
+ }));
111
+ return;
112
+ }
113
+ if ((0, capabilities_1.versionIsOlder)(cliVersion, feature.min_cli)) {
114
+ issues.push(issue({
115
+ field,
116
+ problem: 'this CLI rejects PostgreSQL egress that this deployment supports',
117
+ limit: feature.limit,
118
+ tooling,
119
+ requiredCli: feature.min_cli,
120
+ requiredSdk: feature.min_sdk,
121
+ cliVersion,
122
+ sdkVersion,
123
+ }));
124
+ }
125
+ });
126
+ }
127
+ function needsWideHeaders(manifest) {
128
+ const data = manifest.capabilities?.data;
129
+ const storage = manifest.capabilities?.storage;
130
+ const hasData = !!data && Object.keys(data).length > 0;
131
+ const hasStorage = !!storage && typeof storage === 'object' && Object.keys(storage).length > 0;
132
+ return hasData || hasStorage;
133
+ }
134
+ function checkHeaders({ manifest, capabilities, tooling, cliVersion, sdkVersion, issues, }) {
135
+ const feature = capabilities?.features.http_headers;
136
+ if (!feature || !needsWideHeaders(manifest))
137
+ return;
138
+ const supported = capabilities_1.MANAGED_FUNCTION_CAPABILITIES.features.http_headers.max_bytes;
139
+ if (feature.max_bytes >= supported)
140
+ return;
141
+ issues.push(issue({
142
+ field: feature.field,
143
+ problem: `invocation headers for capabilities.data or capabilities.storage exceed ${feature.max_bytes} bytes and will fail with HTTP 431`,
144
+ limit: feature.limit,
145
+ tooling,
146
+ requiredCli: feature.min_cli,
147
+ requiredSdk: feature.min_sdk,
148
+ cliVersion,
149
+ sdkVersion,
150
+ }));
151
+ }
152
+ function checkSdkTimeout({ manifest, limits, capabilities, tooling, cliVersion, sdkVersion, issues, }) {
153
+ const timeout = manifest.limits?.timeout_seconds;
154
+ if (typeof timeout !== 'number' || !sdkVersion)
155
+ return;
156
+ const feature = capabilities?.features.invoke_timeout ?? capabilities_1.MANAGED_FUNCTION_CAPABILITIES.features.invoke_timeout;
157
+ if (!(0, sdk_range_1.sdkConstraintIsOlder)(sdkVersion, feature.min_sdk))
158
+ return;
159
+ if (timeout <= feature.legacy_sdk_max_seconds)
160
+ return;
161
+ const platformMax = numericCap(limits?.workload?.timeout_seconds)?.max ?? feature.max_seconds;
162
+ issues.push(issue({
163
+ field: feature.field,
164
+ problem: `${timeout} seconds exceeds the installed SDK's ${feature.legacy_sdk_max_seconds} second client cap, behind the platform limit of ${platformMax} seconds`,
165
+ limit: feature.limit,
166
+ tooling,
167
+ requiredCli: feature.min_cli,
168
+ requiredSdk: feature.min_sdk,
169
+ cliVersion,
170
+ sdkVersion,
171
+ }));
172
+ }