@sequenceholdings/managed-functions 0.1.1 → 0.2.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 +120 -6
- package/dist/orm.d.ts +56 -0
- package/dist/orm.js +224 -0
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -1,15 +1,129 @@
|
|
|
1
1
|
# `@sequenceholdings/managed-functions`
|
|
2
2
|
|
|
3
|
-
`@sequenceholdings/managed-functions` is the TypeScript SDK for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`@sequenceholdings/managed-functions` is the TypeScript SDK for Managed
|
|
4
|
+
Functions. It provides caller-scoped ORM access and opt-in caching. The
|
|
5
|
+
`/orm` import loads only the dependency-free ORM execution client, without
|
|
6
|
+
initializing cache, storage, authentication or tracing libraries.
|
|
7
7
|
|
|
8
8
|
Managed Functions is a workflow-agnostic platform primitive. The runtime owns
|
|
9
9
|
generic invocation, isolation, secrets, egress, caching, and observability;
|
|
10
10
|
deployed functions own business logic. Primitive code must not identify a
|
|
11
|
-
function consumer or branch on a function ID.
|
|
12
|
-
|
|
11
|
+
function consumer or branch on a function ID.
|
|
12
|
+
|
|
13
|
+
## ORM as the invoking user
|
|
14
|
+
|
|
15
|
+
Available in SDK **0.2.0 and later**. Install the SDK in the function package;
|
|
16
|
+
the separate ORM authoring/compiler package is not needed in the deployed
|
|
17
|
+
function. Copy the namespace's generated `operations.manifest.json` into that
|
|
18
|
+
package, and keep it synchronized with the applied namespace version.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import type { Request, Response } from '@google-cloud/functions-framework'
|
|
22
|
+
import { createFunctionOrmExecutor, FunctionOrmError } from '@sequenceholdings/managed-functions/orm'
|
|
23
|
+
import manifest from './operations.manifest.json'
|
|
24
|
+
|
|
25
|
+
// Nonsecret configuration: use your deployment's URL, never request input.
|
|
26
|
+
const platformOrigin = 'https://your-environment.example.com'
|
|
27
|
+
|
|
28
|
+
export async function handler(request: Request, response: Response) {
|
|
29
|
+
try {
|
|
30
|
+
// Create inside every invocation; never keep this executor in module state.
|
|
31
|
+
const orm = createFunctionOrmExecutor({
|
|
32
|
+
request,
|
|
33
|
+
baseUrl: platformOrigin,
|
|
34
|
+
namespace: 'directory',
|
|
35
|
+
manifest,
|
|
36
|
+
})
|
|
37
|
+
const profile = await orm.execute('GetMyProfile', {})
|
|
38
|
+
response.json(profile)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof FunctionOrmError) {
|
|
41
|
+
response.status(403).json({ error: error.code })
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
// Do not expose arbitrary exceptions or upstream response bodies.
|
|
45
|
+
response.status(502).json({ error: 'PROFILE_UNAVAILABLE' })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The manifest declares the account and data scope. Replace these illustrative
|
|
51
|
+
names with your provisioned account, namespace, relations and mutation names:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
service_account: directory-runtime
|
|
55
|
+
capabilities:
|
|
56
|
+
data:
|
|
57
|
+
directory:
|
|
58
|
+
tables: [my_profile]
|
|
59
|
+
operations: [UpdateMyProfile]
|
|
60
|
+
query: false
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The deployer needs permission to bind the service account. The human caller
|
|
64
|
+
needs function invocation access plus the namespace runtime grants and operation
|
|
65
|
+
capabilities. `tables` covers read relations, including linked selections and
|
|
66
|
+
policy dependencies; `operations` contains persisted mutation names. These
|
|
67
|
+
declarations restrict access; they do not grant the caller access. Apply matching
|
|
68
|
+
namespace operations before deploying consumers. Open the Managed Functions guide at `/docs/managed-functions/overview` on
|
|
69
|
+
your administrator-supplied platform URL for provisioning, grants and deployment.
|
|
70
|
+
|
|
71
|
+
`createFunctionOrmExecutor({ request, baseUrl, namespace, manifest, fetchImpl? })`
|
|
72
|
+
returns an isolated executor with `execute(operationName, variables?)`. It sends
|
|
73
|
+
only registered operation hashes and variables. The optional `fetchImpl` is for
|
|
74
|
+
testing or an application-owned transport; it must honor `redirect: 'error'`.
|
|
75
|
+
It never retries requests, follows redirects, reads credentials from a request
|
|
76
|
+
body, substitutes a service token, or configures process-wide ORM state. Reuse
|
|
77
|
+
within one handler invocation is supported; discard it when the handler returns.
|
|
78
|
+
|
|
79
|
+
`baseUrl` must be an explicitly configured HTTPS origin, with no path, query,
|
|
80
|
+
fragment or embedded credentials. Loopback HTTP is accepted for a local platform.
|
|
81
|
+
There is no automatic environment URL: the helper never trusts a request header
|
|
82
|
+
or body to choose where it sends a delegated credential.
|
|
83
|
+
|
|
84
|
+
### Caller identity and trust boundary
|
|
85
|
+
|
|
86
|
+
This helper supports direct human invocations through the platform gateway.
|
|
87
|
+
Scheduled/system-runner and service-account invocations fail closed. They do not
|
|
88
|
+
silently become an attached service account or a recorded human initiator.
|
|
89
|
+
No Function UUID, profile lookup, signing key, bearer token or claims decoder is
|
|
90
|
+
required in application code.
|
|
91
|
+
|
|
92
|
+
The platform must control ingress to the deployed handler. The helper consumes
|
|
93
|
+
the request delivered by that gateway; it does **not** authenticate arbitrary
|
|
94
|
+
HTTP requests or make a publicly reachable Functions Framework server safe.
|
|
95
|
+
Its local metadata checks only reject an unsupported or expired invocation mode.
|
|
96
|
+
The ORM server verifies the credential's signature and expiry, checks its
|
|
97
|
+
declared scope, resolves the caller's current permissions, and enforces row
|
|
98
|
+
policies on every operation. The helper exposes no decoded caller identity or
|
|
99
|
+
capabilities for use as a separate authorization decision.
|
|
100
|
+
|
|
101
|
+
`seq-studio functions dev` does not mint delegated user identity. A missing
|
|
102
|
+
context there is expected; validate pure handler logic with supplied test
|
|
103
|
+
executors and verify real caller permissions through a deployed invocation.
|
|
104
|
+
Do not copy real invocation headers into fixtures or logs.
|
|
105
|
+
|
|
106
|
+
### Errors and recovery
|
|
107
|
+
|
|
108
|
+
`FunctionOrmError.code` is stable and safe to return without including the
|
|
109
|
+
exception's stack or request headers:
|
|
110
|
+
|
|
111
|
+
| Code | Recovery |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `INVALID_CONFIGURATION` | Correct the trusted origin or fetch configuration. |
|
|
114
|
+
| `MISSING_CALLER_CONTEXT` | Declare the data binding and invoke through the platform as a signed-in user. |
|
|
115
|
+
| `INVALID_CALLER_CONTEXT` | Verify that the request came through the supported platform runtime. |
|
|
116
|
+
| `UNSUPPORTED_CALLER` | Use a direct human invocation; this helper has no service mode. |
|
|
117
|
+
| `EXPIRED_CALLER_CONTEXT` | Begin a new invocation and construct a new executor. |
|
|
118
|
+
|
|
119
|
+
Invalid manifests, unknown operations and ORM failures propagate `OrmClientError`
|
|
120
|
+
or `OrmOperationError`, also exported from `/orm`. An `OrmOperationError` exposes
|
|
121
|
+
`status` and `code`; handle those instead of returning its upstream message.
|
|
122
|
+
A denied write is still denied; the helper supplies no permission
|
|
123
|
+
fallback. A network error may follow a successful write: use application-level
|
|
124
|
+
idempotency and inspect persisted results before retrying.
|
|
125
|
+
|
|
126
|
+
## Opt-in cache
|
|
13
127
|
|
|
14
128
|
Opt-in caching inside a Managed Function. When the manifest declares a `cache` block, the platform passes a process-reused `context` as the HTTP handler's third argument. Existing two-argument handlers keep working.
|
|
15
129
|
|
package/dist/orm.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
declare class OrmClientError extends Error {
|
|
2
|
+
readonly status: number | null;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
constructor(message: string, status?: number | null);
|
|
5
|
+
}
|
|
6
|
+
interface OperationErrorEntry {
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly extensions?: {
|
|
9
|
+
readonly code?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
declare class OrmOperationError extends OrmClientError {
|
|
13
|
+
readonly code: string | null;
|
|
14
|
+
readonly errors: readonly OperationErrorEntry[];
|
|
15
|
+
readonly name = "OrmOperationError";
|
|
16
|
+
constructor(message: string, status: number | null, code: string | null, errors?: readonly OperationErrorEntry[]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One entry of `operations.manifest.json` (generated by `orm generate`). */
|
|
20
|
+
interface ManifestOperation {
|
|
21
|
+
readonly hash: string;
|
|
22
|
+
readonly kind: 'query' | 'mutation';
|
|
23
|
+
readonly requires: {
|
|
24
|
+
readonly capability?: string;
|
|
25
|
+
readonly role?: string;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
interface OperationsManifest {
|
|
29
|
+
readonly schemaHash: string;
|
|
30
|
+
readonly opsHash?: string;
|
|
31
|
+
readonly operations: Readonly<Record<string, ManifestOperation>>;
|
|
32
|
+
}
|
|
33
|
+
interface OperationExecutor {
|
|
34
|
+
/** Execute a manifest operation by name (or a raw `sha256:` hash). */
|
|
35
|
+
execute<TData = Record<string, unknown>>(op: string, variables?: Record<string, unknown>): Promise<TData>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type FunctionOrmErrorCode = 'INVALID_CONFIGURATION' | 'MISSING_CALLER_CONTEXT' | 'INVALID_CALLER_CONTEXT' | 'UNSUPPORTED_CALLER' | 'EXPIRED_CALLER_CONTEXT';
|
|
39
|
+
declare class FunctionOrmError extends Error {
|
|
40
|
+
readonly code: FunctionOrmErrorCode;
|
|
41
|
+
readonly name = "FunctionOrmError";
|
|
42
|
+
constructor(code: FunctionOrmErrorCode, message: string);
|
|
43
|
+
}
|
|
44
|
+
interface FunctionOrmRequest {
|
|
45
|
+
readonly headers: Pick<Headers, 'get'> | Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
46
|
+
}
|
|
47
|
+
interface FunctionOrmExecutorOptions {
|
|
48
|
+
request: FunctionOrmRequest;
|
|
49
|
+
baseUrl: string;
|
|
50
|
+
namespace: string;
|
|
51
|
+
manifest: unknown;
|
|
52
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
53
|
+
}
|
|
54
|
+
declare function createFunctionOrmExecutor({ request, baseUrl, namespace, manifest, fetchImpl, }: FunctionOrmExecutorOptions): OperationExecutor;
|
|
55
|
+
|
|
56
|
+
export { FunctionOrmError, type FunctionOrmErrorCode, type FunctionOrmExecutorOptions, type FunctionOrmRequest, type OperationExecutor, type OperationsManifest, OrmClientError, OrmOperationError, createFunctionOrmExecutor };
|
package/dist/orm.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/orm.ts
|
|
21
|
+
var orm_exports = {};
|
|
22
|
+
__export(orm_exports, {
|
|
23
|
+
FunctionOrmError: () => FunctionOrmError,
|
|
24
|
+
OrmClientError: () => OrmClientError,
|
|
25
|
+
OrmOperationError: () => OrmOperationError,
|
|
26
|
+
createFunctionOrmExecutor: () => createFunctionOrmExecutor
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(orm_exports);
|
|
29
|
+
|
|
30
|
+
// ../orm/dist/client.js
|
|
31
|
+
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
32
|
+
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
33
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
34
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
35
|
+
var OrmClientError = class extends Error {
|
|
36
|
+
constructor(message, status = null) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.status = status;
|
|
39
|
+
}
|
|
40
|
+
name = "OrmClientError";
|
|
41
|
+
};
|
|
42
|
+
var OrmOperationError = class extends OrmClientError {
|
|
43
|
+
constructor(message, status, code, errors = []) {
|
|
44
|
+
super(message, status);
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.errors = errors;
|
|
47
|
+
}
|
|
48
|
+
name = "OrmOperationError";
|
|
49
|
+
};
|
|
50
|
+
function isRecord(value) {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function isOperationError(value) {
|
|
54
|
+
if (!isRecord(value) || typeof value.message !== "string") return false;
|
|
55
|
+
if (value.extensions === void 0) return true;
|
|
56
|
+
return isRecord(value.extensions) && (value.extensions.code === void 0 || typeof value.extensions.code === "string");
|
|
57
|
+
}
|
|
58
|
+
function readOperationResponse({
|
|
59
|
+
ok,
|
|
60
|
+
status,
|
|
61
|
+
body
|
|
62
|
+
}) {
|
|
63
|
+
const payload = isRecord(body) ? body : null;
|
|
64
|
+
const rawErrors = payload?.errors;
|
|
65
|
+
if (rawErrors !== void 0 && (!Array.isArray(rawErrors) || !rawErrors.every(isOperationError))) {
|
|
66
|
+
throw new OrmOperationError("malformed response: invalid errors", status, null);
|
|
67
|
+
}
|
|
68
|
+
const errors = rawErrors ?? [];
|
|
69
|
+
if (!ok || errors.length > 0) {
|
|
70
|
+
const first = errors[0];
|
|
71
|
+
throw new OrmOperationError(first?.message ?? `HTTP ${status}`, status, first?.extensions?.code ?? null, errors);
|
|
72
|
+
}
|
|
73
|
+
if (!payload || payload.data === void 0 || payload.data === null) {
|
|
74
|
+
throw new OrmOperationError("malformed response: missing data", status, null);
|
|
75
|
+
}
|
|
76
|
+
if (!isRecord(payload.data)) {
|
|
77
|
+
throw new OrmOperationError("malformed response: data must be an object", status, null);
|
|
78
|
+
}
|
|
79
|
+
return payload.data;
|
|
80
|
+
}
|
|
81
|
+
var operationHash = /^sha256:[a-f0-9]{64}$/;
|
|
82
|
+
function readManifestHashes(manifest) {
|
|
83
|
+
if (!isRecord(manifest) || typeof manifest.schemaHash !== "string" || !manifest.schemaHash || !isRecord(manifest.operations)) {
|
|
84
|
+
throw new OrmClientError("invalid operations manifest \u2014 use operations.manifest.json from seq-studio orm generate");
|
|
85
|
+
}
|
|
86
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const [name, entry] of Object.entries(manifest.operations)) {
|
|
88
|
+
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(name) || !isRecord(entry) || typeof entry.hash !== "string" || !operationHash.test(entry.hash) || entry.kind !== "query" && entry.kind !== "mutation") {
|
|
89
|
+
throw new OrmClientError(`invalid operations manifest entry "${name}" \u2014 regenerate the manifest`);
|
|
90
|
+
}
|
|
91
|
+
hashes.set(name, entry.hash);
|
|
92
|
+
}
|
|
93
|
+
return hashes;
|
|
94
|
+
}
|
|
95
|
+
function createOperationExecutorWithTransport({ namespace, manifest, transport }) {
|
|
96
|
+
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
|
+
const hashes = readManifestHashes(manifest);
|
|
98
|
+
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);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/orm.ts
|
|
114
|
+
var FunctionOrmError = class extends Error {
|
|
115
|
+
constructor(code, message) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.code = code;
|
|
118
|
+
}
|
|
119
|
+
name = "FunctionOrmError";
|
|
120
|
+
};
|
|
121
|
+
function readToken({ request }) {
|
|
122
|
+
const { headers } = request;
|
|
123
|
+
let value;
|
|
124
|
+
if (typeof headers.get === "function") {
|
|
125
|
+
value = headers.get(ORM_DATA_TOKEN_HEADER);
|
|
126
|
+
} else {
|
|
127
|
+
const matches = Object.entries(headers).filter(([name]) => name.toLowerCase() === ORM_DATA_TOKEN_HEADER);
|
|
128
|
+
if (matches.length > 1) throw invalidContext();
|
|
129
|
+
value = matches[0]?.[1];
|
|
130
|
+
}
|
|
131
|
+
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
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (typeof value !== "string" || value.length > 32768) throw invalidContext();
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function invalidContext() {
|
|
141
|
+
return new FunctionOrmError("INVALID_CALLER_CONTEXT", "The invocation has no valid function caller context.");
|
|
142
|
+
}
|
|
143
|
+
function record(value) {
|
|
144
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
145
|
+
}
|
|
146
|
+
function callerExpiry({ token }) {
|
|
147
|
+
const parts = /^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]{43})$/.exec(token);
|
|
148
|
+
if (!parts?.[1]) throw invalidContext();
|
|
149
|
+
let claims;
|
|
150
|
+
try {
|
|
151
|
+
claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
152
|
+
} catch {
|
|
153
|
+
throw invalidContext();
|
|
154
|
+
}
|
|
155
|
+
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();
|
|
156
|
+
if (claims.sub.startsWith("service-account|") || claims.sub.endsWith("@clients") || claims.assertedBy != null) {
|
|
157
|
+
throw new FunctionOrmError(
|
|
158
|
+
"UNSUPPORTED_CALLER",
|
|
159
|
+
"This ORM helper requires a direct human invocation. Service-account and system-runner invocations are not supported."
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return claims.exp;
|
|
163
|
+
}
|
|
164
|
+
function requireUnexpired({ expiresAt }) {
|
|
165
|
+
if (expiresAt <= Math.floor(Date.now() / 1e3)) {
|
|
166
|
+
throw new FunctionOrmError(
|
|
167
|
+
"EXPIRED_CALLER_CONTEXT",
|
|
168
|
+
"The caller context has expired. Start a new platform invocation; do not reuse its executor."
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function trustedOrigin({ baseUrl }) {
|
|
173
|
+
let url;
|
|
174
|
+
try {
|
|
175
|
+
url = new URL(baseUrl);
|
|
176
|
+
} catch {
|
|
177
|
+
throw new FunctionOrmError("INVALID_CONFIGURATION", "Configure an absolute platform origin for caller ORM access.");
|
|
178
|
+
}
|
|
179
|
+
const localHttp = url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
180
|
+
if (url.protocol !== "https:" && !localHttp || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
181
|
+
throw new FunctionOrmError(
|
|
182
|
+
"INVALID_CONFIGURATION",
|
|
183
|
+
"Configure an HTTPS platform origin without credentials, path, query or fragment. HTTP is supported only on loopback for local development."
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return url.origin;
|
|
187
|
+
}
|
|
188
|
+
function createFunctionOrmExecutor({
|
|
189
|
+
request,
|
|
190
|
+
baseUrl,
|
|
191
|
+
namespace,
|
|
192
|
+
manifest,
|
|
193
|
+
fetchImpl = globalThis.fetch
|
|
194
|
+
}) {
|
|
195
|
+
const origin = trustedOrigin({ baseUrl });
|
|
196
|
+
const token = readToken({ request });
|
|
197
|
+
const expiresAt = callerExpiry({ token });
|
|
198
|
+
requireUnexpired({ expiresAt });
|
|
199
|
+
if (typeof fetchImpl !== "function") {
|
|
200
|
+
throw new FunctionOrmError("INVALID_CONFIGURATION", "Caller ORM access requires a fetch implementation.");
|
|
201
|
+
}
|
|
202
|
+
return createOperationExecutorWithTransport({
|
|
203
|
+
namespace,
|
|
204
|
+
manifest,
|
|
205
|
+
transport: async ({ namespace: targetNamespace, op, variables }) => {
|
|
206
|
+
requireUnexpired({ expiresAt });
|
|
207
|
+
const response = await fetchImpl(`${origin}/_data/v2/${targetNamespace}`, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: { "Content-Type": "application/json", [ORM_DATA_TOKEN_HEADER]: token },
|
|
210
|
+
body: JSON.stringify({ op, variables }),
|
|
211
|
+
redirect: "error"
|
|
212
|
+
});
|
|
213
|
+
const body = await response.json().catch(() => null);
|
|
214
|
+
return { ok: response.ok, status: response.status, body };
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
219
|
+
0 && (module.exports = {
|
|
220
|
+
FunctionOrmError,
|
|
221
|
+
OrmClientError,
|
|
222
|
+
OrmOperationError,
|
|
223
|
+
createFunctionOrmExecutor
|
|
224
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequenceholdings/managed-functions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Managed Function context and opt-in cache runtime",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"typesVersions": {
|
|
9
|
+
"*": {
|
|
10
|
+
"orm": [
|
|
11
|
+
"dist/orm.d.ts"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
8
15
|
"exports": {
|
|
9
16
|
".": {
|
|
10
17
|
"types": "./dist/index.d.ts",
|
|
@@ -14,6 +21,10 @@
|
|
|
14
21
|
"types": "./dist/config.d.ts",
|
|
15
22
|
"default": "./dist/config.js"
|
|
16
23
|
},
|
|
24
|
+
"./orm": {
|
|
25
|
+
"types": "./dist/orm.d.ts",
|
|
26
|
+
"default": "./dist/orm.js"
|
|
27
|
+
},
|
|
17
28
|
"./runtime": "./dist/runtime.cjs"
|
|
18
29
|
},
|
|
19
30
|
"files": [
|
|
@@ -35,13 +46,15 @@
|
|
|
35
46
|
"@types/node": "^22.0.0",
|
|
36
47
|
"typescript": "^5.6.0",
|
|
37
48
|
"vitest": "^4.1.11",
|
|
38
|
-
"esbuild": "^0.25.0"
|
|
49
|
+
"esbuild": "^0.25.0",
|
|
50
|
+
"tsup": "^8.3.0",
|
|
51
|
+
"@sequenceholdings/orm": "2.1.1"
|
|
39
52
|
},
|
|
40
53
|
"engines": {
|
|
41
54
|
"node": ">=20"
|
|
42
55
|
},
|
|
43
56
|
"scripts": {
|
|
44
|
-
"build": "tsc && node scripts/bundle-runtime.mjs",
|
|
57
|
+
"build": "tsc && node scripts/bundle-orm.mjs && node scripts/bundle-runtime.mjs",
|
|
45
58
|
"type-check": "tsc --noEmit",
|
|
46
59
|
"test": "vitest run"
|
|
47
60
|
}
|