@run402/functions 3.12.0 → 3.14.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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The project-JWT verification keyset, fetched from the gateway at cold start.
3
+ *
4
+ * WHY THIS EXISTS. Until now the runtime read `RUN402_JWT_SECRET` — the
5
+ * PLATFORM signing key — straight out of the Lambda environment. That key is
6
+ * symmetric, so verification material IS signing material: every one of the
7
+ * fleet's tenant Lambdas held the ability to forge `anon_key`/`service_key`
8
+ * for EVERY project on the platform, and those carry no expiry. It also made
9
+ * rotation impossible, because the key arrived at deploy time and could only
10
+ * change by redeploying someone else's code.
11
+ *
12
+ * Fetching a PUBLIC keyset instead fixes both at once: there is nothing here
13
+ * that can sign, and a rotated key reaches the fleet at the next cold start
14
+ * with no redeployment at all.
15
+ *
16
+ * Deliberately mirrors `lib/actor-context-verify.ts`, which established this
17
+ * pattern for the actor-context key — same lazy single-flight fetch, same
18
+ * service-key auth, same "failures are non-fatal, the next request retries".
19
+ *
20
+ * TRANSITION FALLBACK. While `RUN402_JWT_SECRET` is still injected, it is kept
21
+ * as a SECOND verification key, tried after the fetched keyset. This is
22
+ * temporary scaffolding with a defined removal point, and it is load-bearing
23
+ * for exactly one window: after the fleet carries this runtime but BEFORE the
24
+ * gateway's signing key becomes asymmetric, the gateway is still minting HS256
25
+ * tokens that the (asymmetric-only) fetched keyset cannot verify. Removing the
26
+ * fallback before then would break `getUser()` on every project at once.
27
+ * See `openspec/changes/functions-runtime-key-decoupling/design.md`.
28
+ */
29
+ import { createPublicKey } from "node:crypto";
30
+ let cached = null;
31
+ let fetchInFlight = null;
32
+ /**
33
+ * The keys `getUser()` should verify against, most-trusted first.
34
+ *
35
+ * Returns the fetched public keyset followed by the env-injected legacy key
36
+ * when one is present. Order matters only for which key is TRIED first — a
37
+ * token verifies against exactly one key either way, since `verifyWithKey`
38
+ * selects on `kid` and refuses to fall back on a signature mismatch.
39
+ */
40
+ export function projectJwtVerificationKeys() {
41
+ const keys = cached ? [...cached] : [];
42
+ const legacy = legacyEnvKey();
43
+ if (legacy)
44
+ keys.push(legacy);
45
+ return keys;
46
+ }
47
+ /**
48
+ * The env-injected key, as a kid-less LEGACY verification key.
49
+ *
50
+ * `legacy: true` is what lets it verify the kid-less tokens the gateway signs
51
+ * today; once the gateway signs with a `kid`, its tokens select the fetched
52
+ * key by id and never reach this one.
53
+ */
54
+ function legacyEnvKey() {
55
+ const raw = process.env.RUN402_JWT_SECRET;
56
+ if (!raw)
57
+ return null;
58
+ return { key: Buffer.from(raw, "utf8"), alg: "HS256", legacy: true };
59
+ }
60
+ /**
61
+ * Ensure the keyset is loaded before a synchronous verify.
62
+ *
63
+ * Single-flight: concurrent invocations in the same execution environment
64
+ * share one fetch. Cached for the life of the environment, so a warm Lambda
65
+ * pays nothing. Never throws — a failed fetch leaves whatever keys are
66
+ * available (possibly just the env fallback) and the next request retries.
67
+ */
68
+ export async function ensureProjectJwtKeysLoaded() {
69
+ if (cached && cached.length > 0)
70
+ return;
71
+ if (!fetchInFlight) {
72
+ fetchInFlight = fetchKeysFromGateway()
73
+ .then((fetched) => {
74
+ if (fetched.length > 0)
75
+ cached = fetched;
76
+ })
77
+ .catch(() => {
78
+ /* keep whatever we have; the next request retries */
79
+ })
80
+ .finally(() => {
81
+ fetchInFlight = null;
82
+ });
83
+ }
84
+ await fetchInFlight;
85
+ }
86
+ /** True when verification has no key at all — the caller must fail CLOSED
87
+ * rather than treat the request as anonymous-but-fine. */
88
+ export function projectJwtKeysUnavailable() {
89
+ return projectJwtVerificationKeys().length === 0;
90
+ }
91
+ async function fetchKeysFromGateway() {
92
+ const base = process.env.RUN402_API_BASE;
93
+ const serviceKey = process.env.RUN402_SERVICE_KEY;
94
+ if (!base || !serviceKey)
95
+ return [];
96
+ const url = `${base.replace(/\/+$/, "")}/internal/v1/project-jwt-keys`;
97
+ const res = await fetch(url, {
98
+ method: "GET",
99
+ headers: { Authorization: `Bearer ${serviceKey}` },
100
+ });
101
+ if (!res.ok)
102
+ return [];
103
+ const body = (await res.json());
104
+ const out = [];
105
+ for (const jwk of body.keys ?? []) {
106
+ // Only the shape the gateway promises to serve. Anything else — notably
107
+ // anything symmetric — is ignored rather than trusted: this runtime must
108
+ // never end up holding key material that can sign.
109
+ if (jwk.kty !== "EC" || jwk.crv !== "P-256")
110
+ continue;
111
+ if (typeof jwk.x !== "string" || typeof jwk.y !== "string")
112
+ continue;
113
+ try {
114
+ const publicKey = createPublicKey({
115
+ key: { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
116
+ format: "jwk",
117
+ });
118
+ out.push({
119
+ ...(jwk.kid ? { kid: jwk.kid } : {}),
120
+ key: publicKey.export({ type: "spki", format: "der" }),
121
+ alg: "ES256",
122
+ publicKey,
123
+ });
124
+ }
125
+ catch {
126
+ /* skip a malformed entry rather than failing the whole keyset */
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+ /** Test injection. NEVER call from production code. */
132
+ export function _setProjectJwtKeysForTest(keys) {
133
+ cached = keys;
134
+ fetchInFlight = null;
135
+ }
136
+ /**
137
+ * The header carrying the gateway-minted actor token.
138
+ *
139
+ * The runtime FORWARDS this rather than minting its own. Kept here beside the
140
+ * keyset because the two are halves of one contract: the gateway signs both
141
+ * the tokens this runtime verifies and the one it forwards, and the runtime
142
+ * holds nothing that can produce either.
143
+ */
144
+ export const DATA_PLANE_ACTOR_TOKEN_HEADER = "x-run402-actor-token";
145
+ /** Read a header from either shape, case-insensitively. */
146
+ export function readHeader(headers, name) {
147
+ if (!headers)
148
+ return undefined;
149
+ const h = headers;
150
+ if (typeof h.get === "function") {
151
+ return h.get(name) ?? undefined;
152
+ }
153
+ const rec = h;
154
+ const raw = rec[name] ?? rec[name.toLowerCase()] ?? rec[name.toUpperCase()];
155
+ return Array.isArray(raw) ? raw[0] : raw;
156
+ }
157
+ /**
158
+ * The gateway-minted actor bearer for this request, if present.
159
+ *
160
+ * Returns the `Authorization`-ready value. Absent for anonymous requests, and
161
+ * absent on older gateways — callers must fall through to their previous
162
+ * behaviour rather than failing, so a new runtime on an old gateway degrades
163
+ * instead of breaking.
164
+ */
165
+ export function forwardedActorAuthorization(headers) {
166
+ const token = readHeader(headers, DATA_PLANE_ACTOR_TOKEN_HEADER);
167
+ return token ? `Bearer ${token}` : undefined;
168
+ }
169
+ //# sourceMappingURL=project-jwt-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-jwt-keys.js","sourceRoot":"","sources":["../../src/lib/project-jwt-keys.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@run402/functions",
3
- "version": "3.12.0",
3
+ "version": "3.14.0",
4
4
  "description": "In-function helper library for Run402 serverless functions - db, adminDb, getUser, email, ai, assets, verifyWebhook. Auto-bundled into deployed functions; also installable for local TypeScript autocomplete.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",