@funcd-dev/shim 0.0.0-bootstrap.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/blob.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /** Options for a presigned URL: the HTTP method it grants (default GET) and its lifetime (a Go duration
2
+ * string, e.g. "15m"; the driver's default when omitted). A PUT/DELETE URL requires s3::write. */
3
+ export interface SignOptions {
4
+ method?: 'GET' | 'PUT' | 'DELETE';
5
+ expiry?: string;
6
+ }
7
+ /** A function's binding-scoped blob storage (ADR-0127): get/put/delete a bound prefix's objects, list
8
+ * keys, or mint a presigned URL — the blob twin of KVClient. */
9
+ export interface BlobClient {
10
+ get(binding: string, key: string): Promise<Uint8Array | null>;
11
+ put(binding: string, key: string, value: Uint8Array): Promise<void>;
12
+ del(binding: string, key: string): Promise<void>;
13
+ list(binding: string, prefix?: string): Promise<string[]>;
14
+ signedUrl(binding: string, key: string, opts?: SignOptions): Promise<string>;
15
+ }
16
+ /** Build the context.blob client. */
17
+ export declare function makeBlob(): BlobClient;
package/dist/blob.js ADDED
@@ -0,0 +1,72 @@
1
+ // context.blob — function-facing blob over the worker-node local API (HTTP-over-UDS, ADR-0127). Dials the
2
+ // same per-sandbox socket as context.kv/context.invoke (FUNCD_INVOKE_SOCKET); the platform routes /blob/…
3
+ // to the binding-gated, PDP-authorized Facade with the sandbox's function identity (bind-as-grant on
4
+ // spec.blob). Reached only through the built-in shim — no @aws-sdk, no keypair. The blob twin of context.kv;
5
+ // v1 is bytes-in-memory (streaming is a v2 follow-up).
6
+ import http from 'node:http';
7
+ function request(method, path, body) {
8
+ return new Promise((resolve, reject) => {
9
+ const socketPath = process.env.FUNCD_INVOKE_SOCKET;
10
+ if (!socketPath) {
11
+ reject(new Error('context.blob: worker-node local API socket unavailable (FUNCD_INVOKE_SOCKET unset)'));
12
+ return;
13
+ }
14
+ const headers = {};
15
+ if (body)
16
+ headers['content-length'] = body.byteLength;
17
+ const req = http.request({ socketPath, path, method, headers }, (res) => {
18
+ const chunks = [];
19
+ res.on('data', (c) => chunks.push(c));
20
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks) }));
21
+ });
22
+ req.on('error', reject);
23
+ if (body)
24
+ req.end(body);
25
+ else
26
+ req.end();
27
+ });
28
+ }
29
+ const enc = encodeURIComponent;
30
+ // key may be hierarchical ("a/b"); keep the "/" separators (the server's {key...} captures them), encode segments.
31
+ const keyPath = (binding, key) => `/blob/${enc(binding)}/${key.split('/').map(enc).join('/')}`;
32
+ const fail = (verb, r) => new Error(`context.blob.${verb} failed: ${r.status} ${r.body.toString('utf8')}`);
33
+ const ok = (r) => r.status >= 200 && r.status < 300;
34
+ /** Build the context.blob client. */
35
+ export function makeBlob() {
36
+ return {
37
+ async get(binding, key) {
38
+ const r = await request('GET', keyPath(binding, key));
39
+ if (r.status === 404)
40
+ return null;
41
+ if (!ok(r))
42
+ throw fail('get', r);
43
+ return new Uint8Array(r.body);
44
+ },
45
+ async put(binding, key, value) {
46
+ const r = await request('PUT', keyPath(binding, key), Buffer.from(value));
47
+ if (!ok(r))
48
+ throw fail('put', r);
49
+ },
50
+ async del(binding, key) {
51
+ const r = await request('DELETE', keyPath(binding, key));
52
+ if (!ok(r))
53
+ throw fail('del', r);
54
+ },
55
+ async list(binding, prefix) {
56
+ const q = prefix ? `?prefix=${enc(prefix)}` : '';
57
+ const r = await request('GET', `/blob/${enc(binding)}${q}`);
58
+ if (!ok(r))
59
+ throw fail('list', r);
60
+ return JSON.parse(r.body.toString('utf8') || '[]');
61
+ },
62
+ async signedUrl(binding, key, opts) {
63
+ let path = `${keyPath(binding, key)}?sign=1&method=${enc(opts?.method ?? 'GET')}`;
64
+ if (opts?.expiry)
65
+ path += `&expiry=${enc(opts.expiry)}`;
66
+ const r = await request('GET', path);
67
+ if (!ok(r))
68
+ throw fail('signedUrl', r);
69
+ return r.body.toString('utf8');
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,9 @@
1
+ import { type Schema } from 'ts-json-schema-generator';
2
+ export interface ContractBuild {
3
+ /** an ESM module source defining the precompiled __funcdValidate* (null ⇒ no contract). */
4
+ validatorSource: string | null;
5
+ inputSchema: Schema | null;
6
+ outputSchema: Schema | null;
7
+ }
8
+ /** Compile the FuncInput/FuncOutput contract of the TS module at `tsPath`. */
9
+ export declare function buildContract(tsPath: string): ContractBuild;
package/dist/build.js ADDED
@@ -0,0 +1,123 @@
1
+ // funcd Node contract compiler (BUILD-TIME, ADR-0058/0060). The TS analog of funcd_build.py:
2
+ // for an author module declaring `FuncInput`/`FuncOutput` types it generates the JSON Schema
3
+ // (ts-json-schema-generator — closed records, no $ref) and compiles a precompiled validator
4
+ // (AJV-standalone) baked into the bundle as `__funcdValidateInput`/`__funcdValidateOutput`
5
+ // (`(data) => Error[]`, [] ⇒ valid) — exactly what shim.ts's resolveValidators reads.
6
+ //
7
+ // Runs on the push box; NOT bundled into shim.mjs. The TS interfaces vanish at compile (esbuild
8
+ // erases types), so unlike Python there is no source-stripping — only the validator is injected.
9
+ //
10
+ // (ADR-0060 names typia; typia needs a TS *transformer* (ts-patch) that does not compose with the
11
+ // esbuild bundle. ts-json-schema-generator is the no-transformer equivalent — same "TS type ->
12
+ // JSON Schema", same MIT licence; the runtime validator is still AJV-standalone as the ADR says.)
13
+ import Ajv from 'ajv';
14
+ import standaloneCode from 'ajv/dist/standalone/index.js';
15
+ import { createGenerator } from 'ts-json-schema-generator';
16
+ /** Compile the FuncInput/FuncOutput contract of the TS module at `tsPath`. */
17
+ export function buildContract(tsPath) {
18
+ const inputSchema = schemaFor(tsPath, 'FuncInput');
19
+ const outputSchema = schemaFor(tsPath, 'FuncOutput');
20
+ if (!inputSchema && !outputSchema) {
21
+ return { validatorSource: null, inputSchema: null, outputSchema: null };
22
+ }
23
+ // Register each schema under an $id, then standalone-code by id (the multi-export API takes
24
+ // schema ids, not compiled fns). standaloneCode emits `export const _funcdInput = …` — eval-free
25
+ // (compiled here, at build).
26
+ // strictSchema:false ⇒ AJV ignores the `discriminator` keyword we add for the gate (it validates
27
+ // a tagged union via `oneOf` alone; `discriminator` is metadata the Go profile gate requires).
28
+ const ajv = new Ajv({ code: { source: true, esm: true }, allErrors: true, strictSchema: false });
29
+ const refs = {};
30
+ if (inputSchema) {
31
+ ajv.addSchema({ ...stripMeta(inputSchema), $id: 'funcdInput' });
32
+ refs._funcdInput = 'funcdInput';
33
+ }
34
+ if (outputSchema) {
35
+ ajv.addSchema({ ...stripMeta(outputSchema), $id: 'funcdOutput' });
36
+ refs._funcdOutput = 'funcdOutput';
37
+ }
38
+ const base = standaloneCode(ajv, refs);
39
+ const wrap = (name, fn) => `export function ${name}(d) { return ${fn}(d) ? [] : (${fn}.errors ?? []); }\n`;
40
+ const validatorSource = base +
41
+ '\n' +
42
+ (inputSchema ? wrap('__funcdValidateInput', '_funcdInput') : '') +
43
+ (outputSchema ? wrap('__funcdValidateOutput', '_funcdOutput') : '');
44
+ return { validatorSource, inputSchema, outputSchema };
45
+ }
46
+ /** Generate the JSON Schema for `type` from `tsPath`, inlined (no $ref) + closed records — the
47
+ * funcd profile shape; returns null when the type is not declared. */
48
+ function schemaFor(tsPath, type) {
49
+ try {
50
+ const schema = createGenerator({
51
+ path: tsPath,
52
+ type,
53
+ additionalProperties: false, // closed records (the profile forbids open ones)
54
+ topRef: false, // inline the root type (no $ref to a definition)
55
+ expose: 'none', // inline nested types too (no $ref/$defs — the profile forbids $ref)
56
+ skipTypeCheck: true,
57
+ }).createSchema(type);
58
+ const stripped = stripMeta(schema);
59
+ discriminateUnions(stripped); // tagged `anyOf` → the profile's discriminated `oneOf`
60
+ return stripped;
61
+ }
62
+ catch {
63
+ return null; // the type is not declared in this module
64
+ }
65
+ }
66
+ /** Rewrite every tagged `anyOf` into the profile's discriminated `oneOf` + `discriminator`
67
+ * (ADR-0058). ts-json-schema-generator emits a TS union as `anyOf`, but the gate accepts a union
68
+ * only as a tagged `oneOf`; when the branches share a required, distinct-`const` property (the
69
+ * discriminant) the two are equivalent, so convert in place (recursing all nested schemas). */
70
+ function discriminateUnions(node) {
71
+ if (Array.isArray(node)) {
72
+ for (const child of node)
73
+ discriminateUnions(child);
74
+ return;
75
+ }
76
+ if (!node || typeof node !== 'object')
77
+ return;
78
+ const obj = node;
79
+ for (const value of Object.values(obj))
80
+ discriminateUnions(value);
81
+ if (Array.isArray(obj.anyOf)) {
82
+ const tag = discriminatorTag(obj.anyOf);
83
+ if (tag) {
84
+ obj.oneOf = obj.anyOf;
85
+ delete obj.anyOf;
86
+ obj.discriminator = { propertyName: tag };
87
+ }
88
+ }
89
+ }
90
+ /** The property that discriminates an `anyOf`'s branches: present + required + a single `const` in
91
+ * every branch, with distinct const values. Returns null when the union isn't a clean tagged one. */
92
+ function discriminatorTag(branches) {
93
+ const objs = branches.map((b) => (b && typeof b === 'object' ? b : null));
94
+ if (objs.some((b) => b === null))
95
+ return null;
96
+ const firstProps = objs[0]?.properties;
97
+ if (!firstProps)
98
+ return null;
99
+ for (const name of Object.keys(firstProps)) {
100
+ const consts = [];
101
+ const taggedByAll = objs.every((b) => {
102
+ const props = b?.properties;
103
+ const required = b?.required;
104
+ const field = props?.[name];
105
+ if (!props || !Array.isArray(required) || !required.includes(name) || !field || !('const' in field)) {
106
+ return false;
107
+ }
108
+ consts.push(JSON.stringify(field.const));
109
+ return true;
110
+ });
111
+ if (taggedByAll && new Set(consts).size === consts.length)
112
+ return name;
113
+ }
114
+ return null;
115
+ }
116
+ /** Drop JSON-Schema meta keys AJV/the gate don't need (the contract is the type shape). */
117
+ function stripMeta(schema) {
118
+ const { $schema, $ref, definitions, ...rest } = schema;
119
+ void $schema;
120
+ void $ref;
121
+ void definitions;
122
+ return rest;
123
+ }
package/dist/kv.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /** A function's view of its namespace-scoped KV (ADR-0069): get/put/del a binding's key, or list keys. */
2
+ export interface KVClient {
3
+ get(binding: string, key: string): Promise<Uint8Array | null>;
4
+ /** get() decoded as UTF-8 text — the common case (a missing key is null). Saves the caller a
5
+ * TextDecoder dance; put() already accepts a string. */
6
+ getText(binding: string, key: string): Promise<string | null>;
7
+ /** get() decoded as UTF-8 text then JSON-parsed (a missing key is null). The structured-value
8
+ * counterpart of getText; write with put(binding, key, JSON.stringify(value)). */
9
+ getJSON<T = unknown>(binding: string, key: string): Promise<T | null>;
10
+ put(binding: string, key: string, value: Uint8Array | string): Promise<void>;
11
+ del(binding: string, key: string): Promise<void>;
12
+ list(binding: string, prefix?: string): Promise<string[]>;
13
+ }
14
+ /** Build the context.kv client. */
15
+ export declare function makeKV(): KVClient;
package/dist/kv.js ADDED
@@ -0,0 +1,78 @@
1
+ // context.kv — function-facing KV over the worker-node local API (HTTP-over-UDS, ADR-0069). Dials the
2
+ // same per-sandbox socket as context.invoke (FUNCD_INVOKE_SOCKET); the platform routes /kv/… to the
3
+ // PDP-authorized Facade with the sandbox's namespace identity. Reached only through the built-in shim.
4
+ import http from 'node:http';
5
+ function request(method, path, body) {
6
+ return new Promise((resolve, reject) => {
7
+ const socketPath = process.env.FUNCD_INVOKE_SOCKET;
8
+ if (!socketPath) {
9
+ reject(new Error('context.kv: worker-node local API socket unavailable (FUNCD_INVOKE_SOCKET unset)'));
10
+ return;
11
+ }
12
+ const headers = {};
13
+ if (body)
14
+ headers['content-length'] = body.byteLength;
15
+ const req = http.request({ socketPath, path, method, headers }, (res) => {
16
+ const chunks = [];
17
+ res.on('data', (c) => chunks.push(c));
18
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks) }));
19
+ });
20
+ req.on('error', reject);
21
+ if (body)
22
+ req.end(body);
23
+ else
24
+ req.end();
25
+ });
26
+ }
27
+ const enc = encodeURIComponent;
28
+ // key may be hierarchical ("a/b"); keep the "/" separators (the server's {key...} captures them), encode segments.
29
+ const keyPath = (binding, key) => `/kv/${enc(binding)}/${key.split('/').map(enc).join('/')}`;
30
+ const fail = (verb, r) => new Error(`context.kv.${verb} failed: ${r.status} ${r.body.toString('utf8')}`);
31
+ const ok = (r) => r.status >= 200 && r.status < 300;
32
+ /** Build the context.kv client. */
33
+ export function makeKV() {
34
+ return {
35
+ async get(binding, key) {
36
+ const r = await request('GET', keyPath(binding, key));
37
+ if (r.status === 404)
38
+ return null;
39
+ if (!ok(r))
40
+ throw fail('get', r);
41
+ return new Uint8Array(r.body);
42
+ },
43
+ async getText(binding, key) {
44
+ const r = await request('GET', keyPath(binding, key));
45
+ if (r.status === 404)
46
+ return null;
47
+ if (!ok(r))
48
+ throw fail('get', r);
49
+ return r.body.toString('utf8');
50
+ },
51
+ async getJSON(binding, key) {
52
+ const r = await request('GET', keyPath(binding, key));
53
+ if (r.status === 404)
54
+ return null;
55
+ if (!ok(r))
56
+ throw fail('get', r);
57
+ return JSON.parse(r.body.toString('utf8'));
58
+ },
59
+ async put(binding, key, value) {
60
+ const buf = typeof value === 'string' ? Buffer.from(value, 'utf8') : Buffer.from(value);
61
+ const r = await request('PUT', keyPath(binding, key), buf);
62
+ if (!ok(r))
63
+ throw fail('put', r);
64
+ },
65
+ async del(binding, key) {
66
+ const r = await request('DELETE', keyPath(binding, key));
67
+ if (!ok(r))
68
+ throw fail('del', r);
69
+ },
70
+ async list(binding, prefix) {
71
+ const q = prefix ? `?prefix=${enc(prefix)}` : '';
72
+ const r = await request('GET', `/kv/${enc(binding)}${q}`);
73
+ if (!ok(r))
74
+ throw fail('list', r);
75
+ return JSON.parse(r.body.toString('utf8') || '[]');
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,45 @@
1
+ import type { KVClient } from './kv.ts';
2
+ import type { BlobClient } from './blob.ts';
3
+ export type { KVClient } from './kv.ts';
4
+ export type { BlobClient, SignOptions } from './blob.ts';
5
+ /** A CloudEvent — the normalized trigger envelope (ADR-0023). */
6
+ export interface CloudEvent<T = unknown> {
7
+ id: string;
8
+ source: string;
9
+ type: string;
10
+ specversion?: string;
11
+ time?: string;
12
+ datacontenttype?: string;
13
+ subject?: string;
14
+ data?: T;
15
+ /** Forward-compatible extension attributes. */
16
+ [key: string]: unknown;
17
+ }
18
+ /** The per-invocation context the shim passes to the handler. */
19
+ export interface FunctionContext {
20
+ /** Structured log line → stdout (collected by the platform, ADR-0010). */
21
+ log(...args: unknown[]): void;
22
+ /** Synchronously invoke a linked function by its spec.links alias (ADR-0064). The input is
23
+ * validated against the target's contract by the target's shim; its result is returned. Fails
24
+ * closed (rejects) if the caller declares no such link. */
25
+ invoke<I = unknown, O = unknown>(alias: string, input: I): Promise<O>;
26
+ /** Namespace-scoped key-value storage (ADR-0069): get/put/del a binding's key, or list keys. */
27
+ kv: KVClient;
28
+ /** Binding-scoped blob storage (ADR-0127): get/put/del/list a bound prefix's objects, or mint a
29
+ * presigned URL — the blob twin of kv. */
30
+ blob: BlobClient;
31
+ }
32
+ /** A function handler: receives the context + CloudEvent, returns a response (or nothing). */
33
+ export type Handler<In = unknown, Out = unknown> = (context: FunctionContext, event: CloudEvent<In>) => Out | Promise<Out>;
34
+ /** Json — the explicit "arbitrary JSON value" contract type (ADR-0058). Declare `FuncInput`/
35
+ * `FuncOutput = Json`, or a field `payload: Json`, when the shape is genuinely unknown; the
36
+ * generated contract is the empty schema `{}` (accepts any JSON). Typed as `unknown` so the
37
+ * handler must narrow before use — deliberately NOT the unsafe `any`. */
38
+ export type Json = unknown;
39
+ /** A single error from a precompiled validator. Shape kept loose (AJV vs pydantic differ); the
40
+ * shim only inspects the array length and echoes the errors as 422/500 details. */
41
+ export type ValidationError = unknown;
42
+ /** A precompiled, eval-free validator the push build inlines into the bundle from the author's
43
+ * FuncInput/FuncOutput type (ADR-0058). Returns [] when `data` is valid. The shim runs it; it
44
+ * never compiles a schema at runtime. */
45
+ export type Validator = (data: unknown) => ValidationError[];
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@funcd-dev/shim",
3
+ "version": "0.0.0-bootstrap.0",
4
+ "type": "module",
5
+ "description": "Types and the contract build for writing funcd functions in TypeScript",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/pyvvo/funcd-typescript.git",
10
+ "directory": "shim"
11
+ },
12
+ "homepage": "https://github.com/pyvvo/funcd-typescript#readme",
13
+ "engines": {
14
+ "node": ">=22"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/types.d.ts",
19
+ "default": "./dist/types.js"
20
+ },
21
+ "./build": {
22
+ "types": "./dist/build.d.ts",
23
+ "default": "./dist/build.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "node --test --experimental-strip-types test/*.test.ts",
35
+ "build": "yarn build:shim && yarn build:pool && yarn build:lib",
36
+ "build:shim": "esbuild src/shim.ts --bundle --platform=node --format=esm --target=node22 --outfile=shim.mjs --banner:js=\"// GENERATED from shim/src/shim.ts by 'yarn build' (esbuild). Do not edit by hand.\"",
37
+ "build:pool": "esbuild src/pool.ts --bundle --platform=node --format=esm --target=node22 --outfile=pool.mjs --banner:js=\"// GENERATED from shim/src/pool.ts by 'yarn build' (esbuild). Do not edit by hand.\"",
38
+ "build:lib": "tsc -p tsconfig.lib.json"
39
+ },
40
+ "dependencies": {
41
+ "ajv": "^8.20.0",
42
+ "ts-json-schema-generator": "^2.9.0"
43
+ },
44
+ "devDependencies": {
45
+ "@hono/node-server": "^2.0.5",
46
+ "@types/node": "^22.10.0",
47
+ "esbuild": "^0.28.1",
48
+ "hono": "^4.12.25",
49
+ "typescript": "^5.7.0"
50
+ }
51
+ }