@run402/functions 2.4.1 → 2.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 +107 -0
- package/dist/auth.d.ts +47 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +92 -4
- package/dist/auth.js.map +1 -1
- package/dist/cache.d.ts +122 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +243 -0
- package/dist/cache.js.map +1 -0
- package/dist/db.d.ts +26 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +38 -8
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/jwt.d.ts +55 -0
- package/dist/lib/jwt.d.ts.map +1 -0
- package/dist/lib/jwt.js +196 -0
- package/dist/lib/jwt.js.map +1 -0
- package/dist/runtime-context.d.ts +152 -0
- package/dist/runtime-context.d.ts.map +1 -0
- package/dist/runtime-context.js +170 -0
- package/dist/runtime-context.js.map +1 -0
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -98,6 +98,113 @@ if (!user) return new Response("unauthorized", { status: 401 });
|
|
|
98
98
|
|
|
99
99
|
The function's own `RUN402_PROJECT_ID` is used to scope the verification.
|
|
100
100
|
|
|
101
|
+
**Note on `user.role`:** this is the JWT system role (`anon`, `authenticated`, `project_admin`, …) — the same value PostgREST uses to evaluate RLS. It is **not** your application role from a declarative `requireRole` gate (admin/moderator/etc.). For the gate-resolved application role, use `getRole(req)` (see "Function-level auth gates" below). Don't write `getUser(req).role === "admin"` thinking you're checking the gate role — `"admin"` is not a JWT role.
|
|
102
|
+
|
|
103
|
+
## Function-level auth gates
|
|
104
|
+
|
|
105
|
+
A function can declare auth requirements directly on its `FunctionSpec`. When you set `requireAuth: true` or `requireRole: { ... }`, the gateway enforces them **before** invoking your function — unauthorized callers get `401` / `403` without your code running, and the gateway injects the resolved identity into request headers your function can trust.
|
|
106
|
+
|
|
107
|
+
This lets you delete the hand-rolled "fetch JWT, query members table, check role, return 403" boilerplate from every privileged function. Declare the gate in your `FunctionSpec`; read the resolved identity with `getUserId(req)` and `getRole(req)`.
|
|
108
|
+
|
|
109
|
+
### Declaring a gate (deploy spec)
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { run402 } from "@run402/sdk/node";
|
|
113
|
+
|
|
114
|
+
const r = run402();
|
|
115
|
+
await r.project(projectId).apply({
|
|
116
|
+
functions: {
|
|
117
|
+
patch: {
|
|
118
|
+
set: {
|
|
119
|
+
// 1. Authentication only — any valid JWT for this project passes.
|
|
120
|
+
"list-my-items": {
|
|
121
|
+
source: { sha256, size },
|
|
122
|
+
requireAuth: true,
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// 2. Authentication + role check against your members table.
|
|
126
|
+
"delete-content": {
|
|
127
|
+
source: { sha256, size },
|
|
128
|
+
requireRole: {
|
|
129
|
+
table: "members", // project-schema table
|
|
130
|
+
idColumn: "user_id", // FK to the user.id from the JWT
|
|
131
|
+
roleColumn: "role", // column holding the role string
|
|
132
|
+
allowed: ["admin"], // case-sensitive byte-equality allowlist
|
|
133
|
+
cacheTtl: 60, // optional, seconds, default 60, max 600, 0 disables
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// 3. Multi-role — any role in `allowed` passes the gate.
|
|
138
|
+
"moderate-content": {
|
|
139
|
+
source: { sha256, size },
|
|
140
|
+
requireRole: {
|
|
141
|
+
table: "members",
|
|
142
|
+
idColumn: "user_id",
|
|
143
|
+
roleColumn: "role",
|
|
144
|
+
allowed: ["admin", "moderator"],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`requireAuth` and `requireRole` are independent. `requireRole` on its own implies authentication (no valid JWT → 401), then runs the role lookup. `requireAuth: true` alone does a session check with no DB lookup. Set neither to opt out of platform auth (your function owns the check, as today).
|
|
154
|
+
|
|
155
|
+
**Single role-table per release:** all `requireRole` blocks in a single release must share the same `(table, idColumn, roleColumn)` triple. Different `allowed` sets are fine; different tables are not. The gateway rejects conflicting triples at plan time with `INVALID_SPEC`.
|
|
156
|
+
|
|
157
|
+
### Reading the gate result inside your function
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { getUserId, getRole } from "@run402/functions";
|
|
161
|
+
|
|
162
|
+
export default async (req: Request): Promise<Response> => {
|
|
163
|
+
const userId = getUserId(req); // string | null
|
|
164
|
+
const role = getRole(req); // string | null
|
|
165
|
+
|
|
166
|
+
// For a gated function reached through the gateway, both are guaranteed:
|
|
167
|
+
// - getUserId(req) is non-null when requireAuth OR requireRole is on.
|
|
168
|
+
// - getRole(req) is non-null when requireRole is on (and is one of `allowed`).
|
|
169
|
+
// The null case covers local invokes / direct Lambda tests / ungated functions.
|
|
170
|
+
|
|
171
|
+
if (role === "admin") {
|
|
172
|
+
// Privileged path — the gate already verified.
|
|
173
|
+
} else {
|
|
174
|
+
// role === "moderator" (the only other value `allowed` permits).
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Response.json({ ok: true, actor: userId, role });
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The two headers (`x-run402-user-id`, `x-run402-user-role`) are injected by the gateway after the gate passes, and inbound `x-run402-*` headers from the browser are stripped before injection — so the values are trustworthy without any further verification.
|
|
182
|
+
|
|
183
|
+
### Direct vs routed invocation
|
|
184
|
+
|
|
185
|
+
The gate applies to **both** routed (browser via `/your/route`) and direct (`POST /functions/v1/:name` with an API key plus a user JWT) invocation. Direct invocation still requires the project API key at the edge; the gate runs after API-key authentication, against the user JWT.
|
|
186
|
+
|
|
187
|
+
### Deploy-time validation
|
|
188
|
+
|
|
189
|
+
If a `requireRole` block references a table or column that doesn't exist in the project schema at activation time, the deploy fails with `DEPLOY_INVALID_ROLE_GATE` (HTTP 422) **before** flipping the live release. Schema-qualified identifiers (`public.members`), empty `allowed`, and out-of-range `cacheTtl` are rejected earlier at plan time with `INVALID_SPEC` (HTTP 400).
|
|
190
|
+
|
|
191
|
+
### Caching and staleness
|
|
192
|
+
|
|
193
|
+
Role lookups are cached per `(projectId, userId)` for `cacheTtl` seconds (default 60, max 600). **A demoted user keeps the cached role until the TTL expires** — for high-stakes operations where instant revocation matters, set `cacheTtl: 0` to issue a fresh lookup on every request. The cache is bypassed when no `requireRole` gate runs.
|
|
194
|
+
|
|
195
|
+
### Relationship to `getUser`
|
|
196
|
+
|
|
197
|
+
`getUser(req)` decodes the JWT and gives you `{ id, role, email }` where `role` is the JWT system role. The gate-injected headers give you the gate-resolved identity:
|
|
198
|
+
|
|
199
|
+
| Helper | Source | Role meaning |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `getUser(req).id` | JWT `sub` (decoded in-function) | — |
|
|
202
|
+
| `getUser(req).role` | JWT `role` claim | System role (`anon`, `authenticated`, `project_admin`) |
|
|
203
|
+
| `getUserId(req)` | `x-run402-user-id` header (injected by gateway) | — |
|
|
204
|
+
| `getRole(req)` | `x-run402-user-role` header (injected by gateway) | Application role from your `members` table |
|
|
205
|
+
|
|
206
|
+
For a gated function reached through the gateway, `getUserId(req)` and `getUser(req).id` will agree. The gate-side helpers skip the JWT decode (the gateway already did it), so they're slightly cheaper and stringly-typed against the trusted headers; use them when the gate guarantees the identity.
|
|
207
|
+
|
|
101
208
|
## `email.send(...)` — send mail from the project's mailbox
|
|
102
209
|
|
|
103
210
|
Auto-discovers the project's mailbox on first call (the project must already have one — create it once with `run402 email create <slug>` or the `create_mailbox` MCP tool). After that the mailbox id is cached for the function's lifetime.
|
package/dist/auth.d.ts
CHANGED
|
@@ -6,6 +6,52 @@ export interface User {
|
|
|
6
6
|
/**
|
|
7
7
|
* Verify the caller's JWT and return user identity.
|
|
8
8
|
* Returns { id, role, email } or null if unauthenticated/invalid.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: `role` here is the JWT claim (`anon`, `authenticated`,
|
|
11
|
+
* `project_admin`, …) — the PostgREST/RLS system role, NOT the
|
|
12
|
+
* application role from a declarative `requireRole` gate. For the
|
|
13
|
+
* gate-resolved application role, use {@link getRole}.
|
|
9
14
|
*/
|
|
10
|
-
export declare function getUser(req
|
|
15
|
+
export declare function getUser(req?: Request): User | null;
|
|
16
|
+
/**
|
|
17
|
+
* Read the gate-resolved user id from the request.
|
|
18
|
+
*
|
|
19
|
+
* Returns the value of the `x-run402-user-id` request header, which the
|
|
20
|
+
* Run402 gateway injects when a function-level `requireAuth` or
|
|
21
|
+
* `requireRole` gate evaluates successfully on this dispatch. Inbound
|
|
22
|
+
* `x-run402-*` headers from the browser are stripped by the gateway
|
|
23
|
+
* before injection, so the value is trustworthy.
|
|
24
|
+
*
|
|
25
|
+
* Returns `null` when the function has no gate declared, or when the
|
|
26
|
+
* function is invoked outside the gateway (local test harness, direct
|
|
27
|
+
* Lambda invoke). For gated functions reached through the gateway, the
|
|
28
|
+
* value is non-null by construction.
|
|
29
|
+
*
|
|
30
|
+
* This is the gate-side companion to {@link getUser}, which decodes
|
|
31
|
+
* the JWT directly. The two layers are independent: a function with
|
|
32
|
+
* only `requireRole` runs the role lookup against the project's
|
|
33
|
+
* `members` table via gateway-side RLS-bypass; user code does not
|
|
34
|
+
* need to re-decode the JWT.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getUserId(req?: Request): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Read the gate-resolved application role from the request.
|
|
39
|
+
*
|
|
40
|
+
* Returns the value of the `x-run402-user-role` request header, which
|
|
41
|
+
* the Run402 gateway injects when a function-level `requireRole` gate
|
|
42
|
+
* evaluates successfully on this dispatch. The value is the role string
|
|
43
|
+
* from the project-schema `members.role` (or whatever
|
|
44
|
+
* `(table, idColumn, roleColumn)` triple the gate declared), already
|
|
45
|
+
* confirmed to be in `requireRole.allowed`. Inbound `x-run402-*`
|
|
46
|
+
* headers are stripped, so the value is trustworthy.
|
|
47
|
+
*
|
|
48
|
+
* Returns `null` when no `requireRole` gate ran on this dispatch
|
|
49
|
+
* (function has only `requireAuth`, no gate at all, or is invoked
|
|
50
|
+
* outside the gateway).
|
|
51
|
+
*
|
|
52
|
+
* This is the application role, NOT the JWT role from
|
|
53
|
+
* {@link getUser}. The two are independent — see the JSDoc on
|
|
54
|
+
* `getUser` for the distinction.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getRole(req?: Request): string | null;
|
|
11
57
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAsClD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAOtD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAOpD"}
|
package/dist/auth.js
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import jwt from "
|
|
1
|
+
import jwt from "./lib/jwt.js";
|
|
2
2
|
import { config } from "./config.js";
|
|
3
|
+
import { getCurrentContext, taintCacheBypass } from "./runtime-context.js";
|
|
3
4
|
/**
|
|
4
5
|
* Verify the caller's JWT and return user identity.
|
|
5
6
|
* Returns { id, role, email } or null if unauthenticated/invalid.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: `role` here is the JWT claim (`anon`, `authenticated`,
|
|
9
|
+
* `project_admin`, …) — the PostgREST/RLS system role, NOT the
|
|
10
|
+
* application role from a declarative `requireRole` gate. For the
|
|
11
|
+
* gate-resolved application role, use {@link getRole}.
|
|
6
12
|
*/
|
|
7
13
|
export function getUser(req) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
// Capability `astro-ssr-runtime` (v1.52). Taint the cache-bypass flag
|
|
15
|
+
// on the active request context, regardless of whether `getUser`
|
|
16
|
+
// resolves to a user or null — the response now depends on per-request
|
|
17
|
+
// auth state and MUST NOT be cached publicly.
|
|
18
|
+
taintCacheBypass();
|
|
19
|
+
// If no `req` was passed, read auth from the ALS context (the SSR
|
|
20
|
+
// Lambda runtime's `runWithContext` populates `request.headers`).
|
|
21
|
+
// This is what makes `await getUser()` work naturally inside Astro
|
|
22
|
+
// `[slug].astro` frontmatter without any explicit plumbing.
|
|
23
|
+
let authHeader;
|
|
24
|
+
if (req !== undefined) {
|
|
25
|
+
authHeader =
|
|
26
|
+
typeof req.headers.get === "function"
|
|
27
|
+
? req.headers.get("authorization")
|
|
28
|
+
: req.headers?.authorization;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const ctx = getCurrentContext();
|
|
32
|
+
if (ctx === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
const h = ctx.request.headers;
|
|
35
|
+
const raw = h["authorization"] ?? h["Authorization"];
|
|
36
|
+
authHeader = Array.isArray(raw) ? raw[0] : raw;
|
|
37
|
+
}
|
|
11
38
|
if (!authHeader || !authHeader.startsWith("Bearer "))
|
|
12
39
|
return null;
|
|
13
40
|
const token = authHeader.slice(7);
|
|
@@ -21,4 +48,65 @@ export function getUser(req) {
|
|
|
21
48
|
return null;
|
|
22
49
|
}
|
|
23
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Read the gate-resolved user id from the request.
|
|
53
|
+
*
|
|
54
|
+
* Returns the value of the `x-run402-user-id` request header, which the
|
|
55
|
+
* Run402 gateway injects when a function-level `requireAuth` or
|
|
56
|
+
* `requireRole` gate evaluates successfully on this dispatch. Inbound
|
|
57
|
+
* `x-run402-*` headers from the browser are stripped by the gateway
|
|
58
|
+
* before injection, so the value is trustworthy.
|
|
59
|
+
*
|
|
60
|
+
* Returns `null` when the function has no gate declared, or when the
|
|
61
|
+
* function is invoked outside the gateway (local test harness, direct
|
|
62
|
+
* Lambda invoke). For gated functions reached through the gateway, the
|
|
63
|
+
* value is non-null by construction.
|
|
64
|
+
*
|
|
65
|
+
* This is the gate-side companion to {@link getUser}, which decodes
|
|
66
|
+
* the JWT directly. The two layers are independent: a function with
|
|
67
|
+
* only `requireRole` runs the role lookup against the project's
|
|
68
|
+
* `members` table via gateway-side RLS-bypass; user code does not
|
|
69
|
+
* need to re-decode the JWT.
|
|
70
|
+
*/
|
|
71
|
+
export function getUserId(req) {
|
|
72
|
+
if (req !== undefined)
|
|
73
|
+
return req.headers.get("x-run402-user-id");
|
|
74
|
+
const ctx = getCurrentContext();
|
|
75
|
+
if (ctx === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
const raw = ctx.request.headers["x-run402-user-id"];
|
|
78
|
+
if (Array.isArray(raw))
|
|
79
|
+
return raw[0] ?? null;
|
|
80
|
+
return raw ?? null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Read the gate-resolved application role from the request.
|
|
84
|
+
*
|
|
85
|
+
* Returns the value of the `x-run402-user-role` request header, which
|
|
86
|
+
* the Run402 gateway injects when a function-level `requireRole` gate
|
|
87
|
+
* evaluates successfully on this dispatch. The value is the role string
|
|
88
|
+
* from the project-schema `members.role` (or whatever
|
|
89
|
+
* `(table, idColumn, roleColumn)` triple the gate declared), already
|
|
90
|
+
* confirmed to be in `requireRole.allowed`. Inbound `x-run402-*`
|
|
91
|
+
* headers are stripped, so the value is trustworthy.
|
|
92
|
+
*
|
|
93
|
+
* Returns `null` when no `requireRole` gate ran on this dispatch
|
|
94
|
+
* (function has only `requireAuth`, no gate at all, or is invoked
|
|
95
|
+
* outside the gateway).
|
|
96
|
+
*
|
|
97
|
+
* This is the application role, NOT the JWT role from
|
|
98
|
+
* {@link getUser}. The two are independent — see the JSDoc on
|
|
99
|
+
* `getUser` for the distinction.
|
|
100
|
+
*/
|
|
101
|
+
export function getRole(req) {
|
|
102
|
+
if (req !== undefined)
|
|
103
|
+
return req.headers.get("x-run402-user-role");
|
|
104
|
+
const ctx = getCurrentContext();
|
|
105
|
+
if (ctx === undefined)
|
|
106
|
+
return null;
|
|
107
|
+
const raw = ctx.request.headers["x-run402-user-role"];
|
|
108
|
+
if (Array.isArray(raw))
|
|
109
|
+
return raw[0] ?? null;
|
|
110
|
+
return raw ?? null;
|
|
111
|
+
}
|
|
24
112
|
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAQ3E;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,GAAa;IACnC,sEAAsE;IACtE,iEAAiE;IACjE,uEAAuE;IACvE,8CAA8C;IAC9C,gBAAgB,EAAE,CAAC;IAEnB,kEAAkE;IAClE,kEAAkE;IAClE,mEAAmE;IACnE,4DAA4D;IAC5D,IAAI,UAAqC,CAAC;IAC1C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,UAAU;YACR,OAAQ,GAAG,CAAC,OAA6C,CAAC,GAAG,KAAK,UAAU;gBAC1E,CAAC,CAAE,GAAG,CAAC,OAAmB,CAAC,GAAG,CAAC,eAAe,CAAC;gBAC/C,CAAC,CAAE,GAAG,CAAC,OAAyD,EAAE,aAAa,CAAC;IACtF,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;QAChC,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACnC,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC;QAC9B,MAAM,GAAG,GAAG,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC;QACrD,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACjD,CAAC;IACD,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAClE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAKvB,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;QAC7B,IAAI,OAAO,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC;QAC1D,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,SAAS,CAAC,GAAa;IACrC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAClE,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAChC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC9C,OAAO,GAAG,IAAI,IAAI,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,OAAO,CAAC,GAAa;IACnC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACpE,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAChC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACtD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC9C,OAAO,GAAG,IAAI,IAAI,CAAC;AACrB,CAAC"}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR cache invalidation SDK — capability `ssr-isr-cache`.
|
|
3
|
+
*
|
|
4
|
+
* Server-side (function-context) only. Reads the current request's
|
|
5
|
+
* project_id and host from `getCurrentContext()` (AsyncLocalStorage) to
|
|
6
|
+
* scope invalidations safely.
|
|
7
|
+
*
|
|
8
|
+
* Four operations:
|
|
9
|
+
*
|
|
10
|
+
* - `cache.invalidate(urlOrPath)` — delete cache rows for a specific
|
|
11
|
+
* path (or absolute URL). Returns `{ deleted, generation, host, path }`.
|
|
12
|
+
* - `cache.invalidatePrefix({ host, prefix })` — delete all rows
|
|
13
|
+
* under a path prefix on the given host.
|
|
14
|
+
* - `cache.invalidateAll({ host })` — delete all rows on the given
|
|
15
|
+
* host. Atomic with a generation bump.
|
|
16
|
+
* - `cache.invalidateMany(urls)` — bulk invalidate in one round-trip.
|
|
17
|
+
*
|
|
18
|
+
* Host authorization: absolute-URL forms (or explicit `host` in the
|
|
19
|
+
* options bag) MUST be hosts owned by the caller's authenticated
|
|
20
|
+
* project. Cross-project hosts throw `R402_CACHE_INVALIDATION_HOST_FORBIDDEN`.
|
|
21
|
+
*
|
|
22
|
+
* Path-string form requires an active request context (the SDK reads
|
|
23
|
+
* the current host from ALS); calling it outside a handler throws
|
|
24
|
+
* `R402_CACHE_INVALIDATION_HOST_REQUIRED`.
|
|
25
|
+
*
|
|
26
|
+
* Tag-based invalidation is NOT in v1; future v1.5 work.
|
|
27
|
+
*
|
|
28
|
+
* @see openspec/changes/astro-ssr-runtime/specs/ssr-isr-cache/spec.md
|
|
29
|
+
*/
|
|
30
|
+
/** Result envelope returned by every invalidation API. */
|
|
31
|
+
export interface CacheInvalidateResult {
|
|
32
|
+
/** Number of `internal.ssr_cache` rows DELETEd by this call. */
|
|
33
|
+
deleted: number;
|
|
34
|
+
/** Post-increment generation. Each invalidate bumps the per-(project,
|
|
35
|
+
* host) generation counter, which gates in-flight MISS writes from
|
|
36
|
+
* populating stale content after the invalidate. */
|
|
37
|
+
generation: bigint;
|
|
38
|
+
/** The host the invalidation targeted. */
|
|
39
|
+
host: string;
|
|
40
|
+
/** The pathname targeted (single-URL form only); undefined for
|
|
41
|
+
* prefix/all/many. */
|
|
42
|
+
path?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface InvalidatePrefixOptions {
|
|
45
|
+
host: string;
|
|
46
|
+
prefix: string;
|
|
47
|
+
}
|
|
48
|
+
export interface InvalidateAllOptions {
|
|
49
|
+
host: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Structured error thrown when path-string form is called outside an
|
|
53
|
+
* active request context. The caller has no host to scope against.
|
|
54
|
+
* Use the absolute-URL form instead OR move the call into a handler.
|
|
55
|
+
*/
|
|
56
|
+
export declare class CacheInvalidationHostRequiredError extends Error {
|
|
57
|
+
readonly code = "R402_CACHE_INVALIDATION_HOST_REQUIRED";
|
|
58
|
+
readonly docs = "https://docs.run402.com/cache/errors#invalidation-host-required";
|
|
59
|
+
readonly suggestedFix = "Pass an absolute URL (e.g., `new URL('https://eagles.kychon.com/the-guys')`) OR move the call into a request handler so the SDK can resolve the current host.";
|
|
60
|
+
constructor();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Structured error thrown when an invalidation targets a host that is
|
|
64
|
+
* not owned by the caller's authenticated project. Prevents project A
|
|
65
|
+
* from invalidating project B's cache.
|
|
66
|
+
*/
|
|
67
|
+
export declare class CacheInvalidationHostForbiddenError extends Error {
|
|
68
|
+
readonly code = "R402_CACHE_INVALIDATION_HOST_FORBIDDEN";
|
|
69
|
+
readonly docs = "https://docs.run402.com/cache/errors#invalidation-host-forbidden";
|
|
70
|
+
readonly host: string;
|
|
71
|
+
readonly suggestedFix: string;
|
|
72
|
+
constructor(host: string);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Invalidate a single cached SSR response.
|
|
76
|
+
*
|
|
77
|
+
* Accepts either:
|
|
78
|
+
* - a path string (e.g., `'/the-guys'`) — the SDK reads the current
|
|
79
|
+
* host from the active request context (throws
|
|
80
|
+
* `CacheInvalidationHostRequiredError` if no context)
|
|
81
|
+
* - an absolute URL (e.g., `new URL('https://eagles.kychon.com/the-guys')`)
|
|
82
|
+
* — multi-host admin scenarios (host MUST be owned by caller's
|
|
83
|
+
* project; throws `CacheInvalidationHostForbiddenError` otherwise)
|
|
84
|
+
*
|
|
85
|
+
* Deletes GET and HEAD rows for ALL locales and ALL release ids matching
|
|
86
|
+
* the exact canonical pathname + normalized search on the target host.
|
|
87
|
+
*
|
|
88
|
+
* The DELETE and the per-(project, host) generation increment happen in
|
|
89
|
+
* the same transaction; the post-increment generation is returned.
|
|
90
|
+
*/
|
|
91
|
+
export declare function invalidate(urlOrPath: string | URL): Promise<CacheInvalidateResult>;
|
|
92
|
+
/**
|
|
93
|
+
* Invalidate all cache rows under a path prefix on the given host.
|
|
94
|
+
*/
|
|
95
|
+
export declare function invalidatePrefix(opts: InvalidatePrefixOptions): Promise<CacheInvalidateResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Invalidate all cache rows for the given host (entire-host purge).
|
|
98
|
+
* Useful for catastrophic content changes (nav restructure, layout
|
|
99
|
+
* update, etc.) where targeted invalidation would be impractical.
|
|
100
|
+
*/
|
|
101
|
+
export declare function invalidateAll(opts: InvalidateAllOptions): Promise<CacheInvalidateResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Bulk-invalidate many URLs in a single round-trip. Path-string forms
|
|
104
|
+
* use the current request context's host; absolute URLs are scoped
|
|
105
|
+
* individually.
|
|
106
|
+
*
|
|
107
|
+
* Returns a SUMMARY envelope with the total deleted count. Per-URL
|
|
108
|
+
* results are also available on the optional `results` field of the
|
|
109
|
+
* returned object (not in the result type return for the common case;
|
|
110
|
+
* use the internal endpoint if you need per-URL precision).
|
|
111
|
+
*/
|
|
112
|
+
export declare function invalidateMany(urls: Array<string | URL>): Promise<CacheInvalidateResult>;
|
|
113
|
+
/** The full cache namespace exported via the `cache` named export from
|
|
114
|
+
* `@run402/functions`. */
|
|
115
|
+
export declare const cache: {
|
|
116
|
+
readonly invalidate: typeof invalidate;
|
|
117
|
+
readonly invalidatePrefix: typeof invalidatePrefix;
|
|
118
|
+
readonly invalidateAll: typeof invalidateAll;
|
|
119
|
+
readonly invalidateMany: typeof invalidateMany;
|
|
120
|
+
};
|
|
121
|
+
export type Cache = typeof cache;
|
|
122
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,0DAA0D;AAC1D,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB;;yDAEqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb;2BACuB;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,qBAAa,kCAAmC,SAAQ,KAAK;IAC3D,QAAQ,CAAC,IAAI,2CAA2C;IACxD,QAAQ,CAAC,IAAI,qEAAqE;IAClF,QAAQ,CAAC,YAAY,mKAC6I;;CASnK;AAED;;;;GAIG;AACH,qBAAa,mCAAoC,SAAQ,KAAK;IAC5D,QAAQ,CAAC,IAAI,4CAA4C;IACzD,QAAQ,CAAC,IAAI,sEAAsE;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,IAAI,EAAE,MAAM;CAMzB;AA4DD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAsCxF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAepG;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAW9F;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA0B9F;AAED;2BAC2B;AAC3B,eAAO,MAAM,KAAK;;;;;CAKR,CAAC;AAEX,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR cache invalidation SDK — capability `ssr-isr-cache`.
|
|
3
|
+
*
|
|
4
|
+
* Server-side (function-context) only. Reads the current request's
|
|
5
|
+
* project_id and host from `getCurrentContext()` (AsyncLocalStorage) to
|
|
6
|
+
* scope invalidations safely.
|
|
7
|
+
*
|
|
8
|
+
* Four operations:
|
|
9
|
+
*
|
|
10
|
+
* - `cache.invalidate(urlOrPath)` — delete cache rows for a specific
|
|
11
|
+
* path (or absolute URL). Returns `{ deleted, generation, host, path }`.
|
|
12
|
+
* - `cache.invalidatePrefix({ host, prefix })` — delete all rows
|
|
13
|
+
* under a path prefix on the given host.
|
|
14
|
+
* - `cache.invalidateAll({ host })` — delete all rows on the given
|
|
15
|
+
* host. Atomic with a generation bump.
|
|
16
|
+
* - `cache.invalidateMany(urls)` — bulk invalidate in one round-trip.
|
|
17
|
+
*
|
|
18
|
+
* Host authorization: absolute-URL forms (or explicit `host` in the
|
|
19
|
+
* options bag) MUST be hosts owned by the caller's authenticated
|
|
20
|
+
* project. Cross-project hosts throw `R402_CACHE_INVALIDATION_HOST_FORBIDDEN`.
|
|
21
|
+
*
|
|
22
|
+
* Path-string form requires an active request context (the SDK reads
|
|
23
|
+
* the current host from ALS); calling it outside a handler throws
|
|
24
|
+
* `R402_CACHE_INVALIDATION_HOST_REQUIRED`.
|
|
25
|
+
*
|
|
26
|
+
* Tag-based invalidation is NOT in v1; future v1.5 work.
|
|
27
|
+
*
|
|
28
|
+
* @see openspec/changes/astro-ssr-runtime/specs/ssr-isr-cache/spec.md
|
|
29
|
+
*/
|
|
30
|
+
import { config } from "./config.js";
|
|
31
|
+
import { getCurrentContext } from "./runtime-context.js";
|
|
32
|
+
/**
|
|
33
|
+
* Structured error thrown when path-string form is called outside an
|
|
34
|
+
* active request context. The caller has no host to scope against.
|
|
35
|
+
* Use the absolute-URL form instead OR move the call into a handler.
|
|
36
|
+
*/
|
|
37
|
+
export class CacheInvalidationHostRequiredError extends Error {
|
|
38
|
+
code = "R402_CACHE_INVALIDATION_HOST_REQUIRED";
|
|
39
|
+
docs = "https://docs.run402.com/cache/errors#invalidation-host-required";
|
|
40
|
+
suggestedFix = "Pass an absolute URL (e.g., `new URL('https://eagles.kychon.com/the-guys')`) OR move the call into a request handler so the SDK can resolve the current host.";
|
|
41
|
+
constructor() {
|
|
42
|
+
super("cache.invalidate(path) called outside a request context. " +
|
|
43
|
+
"Path-string form needs the current host from request scope.");
|
|
44
|
+
this.name = "CacheInvalidationHostRequiredError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Structured error thrown when an invalidation targets a host that is
|
|
49
|
+
* not owned by the caller's authenticated project. Prevents project A
|
|
50
|
+
* from invalidating project B's cache.
|
|
51
|
+
*/
|
|
52
|
+
export class CacheInvalidationHostForbiddenError extends Error {
|
|
53
|
+
code = "R402_CACHE_INVALIDATION_HOST_FORBIDDEN";
|
|
54
|
+
docs = "https://docs.run402.com/cache/errors#invalidation-host-forbidden";
|
|
55
|
+
host;
|
|
56
|
+
suggestedFix;
|
|
57
|
+
constructor(host) {
|
|
58
|
+
super(`host ${host} is not owned by the current project`);
|
|
59
|
+
this.name = "CacheInvalidationHostForbiddenError";
|
|
60
|
+
this.host = host;
|
|
61
|
+
this.suggestedFix = `Use a host attached to your project. List your project's hosts with \`r.domains.list()\` or check Run402 dashboard. The cache layer rejects cross-project invalidations to preserve tenant isolation.`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Internal: call the gateway's cache invalidation endpoint with the
|
|
66
|
+
* resolved scope. The gateway enforces project ownership of the host
|
|
67
|
+
* server-side; this function relies on that enforcement but ALSO
|
|
68
|
+
* validates the response shape.
|
|
69
|
+
*/
|
|
70
|
+
async function callCacheInvalidate(body) {
|
|
71
|
+
const url = `${config.API_BASE}/cache/v1/invalidate`;
|
|
72
|
+
const response = await fetch(url, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
// The gateway resolves the caller's project from the service key.
|
|
77
|
+
// Cache invalidation is server-side only; the SDK is bundled into
|
|
78
|
+
// user functions and runs with RUN402_SERVICE_KEY in scope.
|
|
79
|
+
Authorization: "Bearer " + config.SERVICE_KEY,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(body),
|
|
82
|
+
});
|
|
83
|
+
if (response.status === 403) {
|
|
84
|
+
const errBody = (await response.json().catch(() => ({})));
|
|
85
|
+
const host = typeof errBody.host === "string" ? errBody.host : (body.host ?? "");
|
|
86
|
+
throw new CacheInvalidationHostForbiddenError(host);
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const errBody = await response.text().catch(() => "");
|
|
90
|
+
throw new Error(`cache.invalidate(${body.kind}) failed: HTTP ${response.status} ${errBody.slice(0, 200)}`);
|
|
91
|
+
}
|
|
92
|
+
const json = (await response.json());
|
|
93
|
+
return {
|
|
94
|
+
deleted: json.deleted,
|
|
95
|
+
generation: BigInt(json.generation),
|
|
96
|
+
host: json.host,
|
|
97
|
+
path: json.path,
|
|
98
|
+
results: json.results?.map((r) => ({
|
|
99
|
+
...r,
|
|
100
|
+
generation: BigInt(r.generation),
|
|
101
|
+
})),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Invalidate a single cached SSR response.
|
|
106
|
+
*
|
|
107
|
+
* Accepts either:
|
|
108
|
+
* - a path string (e.g., `'/the-guys'`) — the SDK reads the current
|
|
109
|
+
* host from the active request context (throws
|
|
110
|
+
* `CacheInvalidationHostRequiredError` if no context)
|
|
111
|
+
* - an absolute URL (e.g., `new URL('https://eagles.kychon.com/the-guys')`)
|
|
112
|
+
* — multi-host admin scenarios (host MUST be owned by caller's
|
|
113
|
+
* project; throws `CacheInvalidationHostForbiddenError` otherwise)
|
|
114
|
+
*
|
|
115
|
+
* Deletes GET and HEAD rows for ALL locales and ALL release ids matching
|
|
116
|
+
* the exact canonical pathname + normalized search on the target host.
|
|
117
|
+
*
|
|
118
|
+
* The DELETE and the per-(project, host) generation increment happen in
|
|
119
|
+
* the same transaction; the post-increment generation is returned.
|
|
120
|
+
*/
|
|
121
|
+
export async function invalidate(urlOrPath) {
|
|
122
|
+
let host;
|
|
123
|
+
let path;
|
|
124
|
+
if (urlOrPath instanceof URL) {
|
|
125
|
+
host = urlOrPath.host.toLowerCase();
|
|
126
|
+
path = urlOrPath.pathname + urlOrPath.search;
|
|
127
|
+
}
|
|
128
|
+
else if (typeof urlOrPath === "string" && urlOrPath.startsWith("http://")) {
|
|
129
|
+
// Allow http:// only for local-dev parity, but the gateway will
|
|
130
|
+
// typically reject it.
|
|
131
|
+
const u = new URL(urlOrPath);
|
|
132
|
+
host = u.host.toLowerCase();
|
|
133
|
+
path = u.pathname + u.search;
|
|
134
|
+
}
|
|
135
|
+
else if (typeof urlOrPath === "string" && urlOrPath.startsWith("https://")) {
|
|
136
|
+
const u = new URL(urlOrPath);
|
|
137
|
+
host = u.host.toLowerCase();
|
|
138
|
+
path = u.pathname + u.search;
|
|
139
|
+
}
|
|
140
|
+
else if (typeof urlOrPath === "string" && urlOrPath.startsWith("/")) {
|
|
141
|
+
// Path-string form: requires active request context to resolve host.
|
|
142
|
+
const ctx = getCurrentContext();
|
|
143
|
+
if (ctx === undefined || !ctx.active.value) {
|
|
144
|
+
throw new CacheInvalidationHostRequiredError();
|
|
145
|
+
}
|
|
146
|
+
host = ctx.host.toLowerCase();
|
|
147
|
+
path = urlOrPath;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
throw new Error(`cache.invalidate: argument must be a URL, an "https://" URL string, or a path starting with "/". Got: ${typeof urlOrPath === "string" ? `"${urlOrPath}"` : typeof urlOrPath}`);
|
|
151
|
+
}
|
|
152
|
+
const result = await callCacheInvalidate({ kind: "exact", host, path });
|
|
153
|
+
return {
|
|
154
|
+
deleted: result.deleted,
|
|
155
|
+
generation: result.generation,
|
|
156
|
+
host: result.host,
|
|
157
|
+
path: result.path ?? path,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Invalidate all cache rows under a path prefix on the given host.
|
|
162
|
+
*/
|
|
163
|
+
export async function invalidatePrefix(opts) {
|
|
164
|
+
if (!opts.host)
|
|
165
|
+
throw new Error("cache.invalidatePrefix: host is required");
|
|
166
|
+
if (!opts.prefix || !opts.prefix.startsWith("/")) {
|
|
167
|
+
throw new Error("cache.invalidatePrefix: prefix must start with '/'");
|
|
168
|
+
}
|
|
169
|
+
const result = await callCacheInvalidate({
|
|
170
|
+
kind: "prefix",
|
|
171
|
+
host: opts.host.toLowerCase(),
|
|
172
|
+
prefix: opts.prefix,
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
deleted: result.deleted,
|
|
176
|
+
generation: result.generation,
|
|
177
|
+
host: result.host,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Invalidate all cache rows for the given host (entire-host purge).
|
|
182
|
+
* Useful for catastrophic content changes (nav restructure, layout
|
|
183
|
+
* update, etc.) where targeted invalidation would be impractical.
|
|
184
|
+
*/
|
|
185
|
+
export async function invalidateAll(opts) {
|
|
186
|
+
if (!opts.host)
|
|
187
|
+
throw new Error("cache.invalidateAll: host is required");
|
|
188
|
+
const result = await callCacheInvalidate({
|
|
189
|
+
kind: "all",
|
|
190
|
+
host: opts.host.toLowerCase(),
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
deleted: result.deleted,
|
|
194
|
+
generation: result.generation,
|
|
195
|
+
host: result.host,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Bulk-invalidate many URLs in a single round-trip. Path-string forms
|
|
200
|
+
* use the current request context's host; absolute URLs are scoped
|
|
201
|
+
* individually.
|
|
202
|
+
*
|
|
203
|
+
* Returns a SUMMARY envelope with the total deleted count. Per-URL
|
|
204
|
+
* results are also available on the optional `results` field of the
|
|
205
|
+
* returned object (not in the result type return for the common case;
|
|
206
|
+
* use the internal endpoint if you need per-URL precision).
|
|
207
|
+
*/
|
|
208
|
+
export async function invalidateMany(urls) {
|
|
209
|
+
if (!Array.isArray(urls) || urls.length === 0) {
|
|
210
|
+
return { deleted: 0, generation: 0n, host: "" };
|
|
211
|
+
}
|
|
212
|
+
// Resolve each URL to its absolute form, using the current context
|
|
213
|
+
// for path-only entries.
|
|
214
|
+
const ctx = getCurrentContext();
|
|
215
|
+
const resolved = urls.map((u) => {
|
|
216
|
+
if (u instanceof URL)
|
|
217
|
+
return u.toString();
|
|
218
|
+
if (u.startsWith("https://") || u.startsWith("http://"))
|
|
219
|
+
return u;
|
|
220
|
+
if (u.startsWith("/")) {
|
|
221
|
+
if (ctx === undefined || !ctx.active.value) {
|
|
222
|
+
throw new CacheInvalidationHostRequiredError();
|
|
223
|
+
}
|
|
224
|
+
return `https://${ctx.host}${u}`;
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`cache.invalidateMany: invalid entry "${u}"`);
|
|
227
|
+
});
|
|
228
|
+
const result = await callCacheInvalidate({ kind: "many", urls: resolved });
|
|
229
|
+
return {
|
|
230
|
+
deleted: result.deleted,
|
|
231
|
+
generation: result.generation,
|
|
232
|
+
host: result.host ?? "",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** The full cache namespace exported via the `cache` named export from
|
|
236
|
+
* `@run402/functions`. */
|
|
237
|
+
export const cache = {
|
|
238
|
+
invalidate,
|
|
239
|
+
invalidatePrefix,
|
|
240
|
+
invalidateAll,
|
|
241
|
+
invalidateMany,
|
|
242
|
+
};
|
|
243
|
+
//# sourceMappingURL=cache.js.map
|