@dwk/solid-pod 0.1.0-beta.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/LICENSE +15 -0
- package/README.md +108 -0
- package/dist/auth.d.ts +33 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +160 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +181 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +74 -0
- package/dist/config.js.map +1 -0
- package/dist/encoding.d.ts +13 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +31 -0
- package/dist/encoding.js.map +1 -0
- package/dist/gc.d.ts +22 -0
- package/dist/gc.d.ts.map +1 -0
- package/dist/gc.js +33 -0
- package/dist/gc.js.map +1 -0
- package/dist/handler.d.ts +20 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +155 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +36 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +120 -0
- package/dist/jwt.js.map +1 -0
- package/dist/ldp.d.ts +37 -0
- package/dist/ldp.d.ts.map +1 -0
- package/dist/ldp.js +85 -0
- package/dist/ldp.js.map +1 -0
- package/dist/log.d.ts +55 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +51 -0
- package/dist/log.js.map +1 -0
- package/dist/negotiation.d.ts +23 -0
- package/dist/negotiation.d.ts.map +1 -0
- package/dist/negotiation.js +80 -0
- package/dist/negotiation.js.map +1 -0
- package/dist/patch.d.ts +80 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +425 -0
- package/dist/patch.js.map +1 -0
- package/dist/pod.d.ts +20 -0
- package/dist/pod.d.ts.map +1 -0
- package/dist/pod.js +860 -0
- package/dist/pod.js.map +1 -0
- package/dist/wac.d.ts +33 -0
- package/dist/wac.d.ts.map +1 -0
- package/dist/wac.js +84 -0
- package/dist/wac.js.map +1 -0
- package/package.json +55 -0
- package/src/auth.ts +203 -0
- package/src/config.ts +254 -0
- package/src/encoding.ts +32 -0
- package/src/gc.ts +47 -0
- package/src/handler.ts +199 -0
- package/src/index.ts +32 -0
- package/src/jwt.ts +166 -0
- package/src/ldp.ts +99 -0
- package/src/log.ts +59 -0
- package/src/negotiation.ts +97 -0
- package/src/patch.ts +539 -0
- package/src/pod.ts +1195 -0
- package/src/wac.ts +119 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David W. Keith
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# `@dwk/solid-pod`
|
|
2
|
+
|
|
3
|
+
> Edge-native Solid Pod: LDP verbs, content negotiation, N3 Patch, WAC, notifications. Ships the per-pod Durable Object.
|
|
4
|
+
|
|
5
|
+
Part of the [`@dwk` IndieWeb + Solid cohort](../../README.md). See the
|
|
6
|
+
[package specification](../../spec/packages/solid-pod.md) for the full requirements.
|
|
7
|
+
|
|
8
|
+
An edge-native [Solid](https://solidproject.org/TR/protocol) Pod: a stateless
|
|
9
|
+
Worker front door over a per-pod **Durable Object** that is the consistency,
|
|
10
|
+
authorization, and notification authority, with **R2** for blob bodies. This is
|
|
11
|
+
the only `@dwk` package that ships a Durable Object. It composes the reusable
|
|
12
|
+
libraries [`@dwk/dpop`](../dpop) (edge DPoP validation), [`@dwk/rdf`](../rdf)
|
|
13
|
+
(Turtle/JSON-LD), [`@dwk/wac`](../wac) (access control), and
|
|
14
|
+
[`@dwk/store`](../store) (DO-SQLite quads + R2 copy-on-write blobs).
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
- **LDP** — `GET / HEAD / OPTIONS / PUT / POST / PATCH / DELETE` with resource
|
|
19
|
+
and basic-container semantics (`ldp:contains`).
|
|
20
|
+
- **Content negotiation** — Turtle and JSON-LD (plus the other Turtle-family
|
|
21
|
+
types) on read, via `@dwk/rdf`.
|
|
22
|
+
- **N3 Patch / `application/sparql-update`** — `solid:where` is matched against
|
|
23
|
+
the current graph with minimal (non-SPARQL) semantics: no exact single
|
|
24
|
+
binding ⇒ **409**; `deletes` then `inserts` apply in one SQLite transaction.
|
|
25
|
+
- **Web Access Control** — walks to the nearest effective `.acl`
|
|
26
|
+
(`acl:accessTo` / `acl:default`), evaluating `Read`/`Write`/`Append`/`Control`,
|
|
27
|
+
agents, groups, `acl:agentClass foaf:Agent`, and `acl:origin` via `@dwk/wac`.
|
|
28
|
+
`Append` authorizes insert-only patches; any delete requires `Write`. The pod
|
|
29
|
+
`owner` always has full access (bootstraps `.acl` management).
|
|
30
|
+
- **Auth (Resource Server)** — DPoP-bound bearer tokens validated at the edge
|
|
31
|
+
(issuer JWKS pinned by `kid`, header `typ: at+jwt`, `aud`/`exp`/`nbf`/`webid`,
|
|
32
|
+
proof `htu`/`htm`/`ath`/`cnf.jkt`). Strict
|
|
33
|
+
single-use `jti` replay is enforced in the DO for **writes**, pruned by expiry;
|
|
34
|
+
reads do not consume a `jti` (a documented tradeoff).
|
|
35
|
+
- **Concurrency** — all writes funnel through the single-threaded DO; the
|
|
36
|
+
`If-Match` / `If-None-Match` (create-only) check and the write are
|
|
37
|
+
TOCTOU-free, evaluated inside the store's write transaction. Deleting a
|
|
38
|
+
non-empty container is likewise rejected inside that transaction.
|
|
39
|
+
- **Oversized / binary bodies** — content-addressed R2 copy-on-write with an
|
|
40
|
+
atomic DO pointer flip; orphaned keys are reclaimed by an out-of-band GC cron
|
|
41
|
+
(`createSolidPodGc`), never by waking a DO.
|
|
42
|
+
- **Notifications** — Solid Notifications over WebSocket channels on the DO's
|
|
43
|
+
hibernatable WebSockets (v1 channels carry the changed resource IRI only).
|
|
44
|
+
|
|
45
|
+
v1 is a **Resource Server only** (no OIDC OP) and runs **one Durable Object per
|
|
46
|
+
pod** (no sharding).
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createSolidPod, createSolidPodGc, SolidPodObject } from "@dwk/solid-pod";
|
|
52
|
+
|
|
53
|
+
const pod = createSolidPod({
|
|
54
|
+
baseUrl: "https://pod.example",
|
|
55
|
+
issuer: "https://issuer.example",
|
|
56
|
+
jwksUri: "https://issuer.example/jwks",
|
|
57
|
+
owner: "https://pod.example/profile/card#me",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const gc = createSolidPodGc({
|
|
61
|
+
baseUrl: "https://pod.example",
|
|
62
|
+
gcSafetyWindowMs: 300_000,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
fetch: pod,
|
|
67
|
+
scheduled: gc,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Bind the per-pod Durable Object class in your Worker.
|
|
71
|
+
export { SolidPodObject };
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Required bindings & `wrangler.toml`
|
|
75
|
+
|
|
76
|
+
```toml
|
|
77
|
+
compatibility_date = "2025-01-01"
|
|
78
|
+
# N3.js (via @dwk/rdf) uses Node's stream/buffer; the pod runs the parser in the DO.
|
|
79
|
+
compatibility_flags = ["nodejs_compat"]
|
|
80
|
+
|
|
81
|
+
[[durable_objects.bindings]]
|
|
82
|
+
name = "POD"
|
|
83
|
+
class_name = "SolidPodObject"
|
|
84
|
+
|
|
85
|
+
[[migrations]]
|
|
86
|
+
tag = "v1"
|
|
87
|
+
new_sqlite_classes = ["SolidPodObject"]
|
|
88
|
+
|
|
89
|
+
[[r2_buckets]]
|
|
90
|
+
binding = "BLOBS"
|
|
91
|
+
bucket_name = "solid-pod-blobs"
|
|
92
|
+
|
|
93
|
+
# Optional: shared D1 table for out-of-band R2 garbage collection.
|
|
94
|
+
[[d1_databases]]
|
|
95
|
+
binding = "GC_DB"
|
|
96
|
+
database_name = "solid-pod-gc"
|
|
97
|
+
|
|
98
|
+
[triggers]
|
|
99
|
+
crons = ["*/5 * * * *"]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `POD` (Durable Object) and `BLOBS` (R2) bindings are required; the handler
|
|
103
|
+
**fails loudly** at startup if either is missing. `GC_DB` (D1) is optional — when
|
|
104
|
+
bound, the DO forwards orphaned blob keys to it for the GC cron to reclaim.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
[ISC](../../LICENSE)
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge authentication for the Resource Server: validate a DPoP-bound bearer
|
|
3
|
+
* token and its proof at the Worker front door, before any request reaches the
|
|
4
|
+
* per-pod Durable Object.
|
|
5
|
+
*
|
|
6
|
+
* A request with no credentials is allowed through unauthenticated — WAC then
|
|
7
|
+
* decides whether the resource is public. A request that *presents* credentials
|
|
8
|
+
* must pass fully or it is rejected `401`: the token signature (issuer JWKS),
|
|
9
|
+
* the `iss` / `aud` / `exp` / `webid` claims, and the RFC 9449 DPoP proof
|
|
10
|
+
* binding (`htu` / `htm` / `ath` / `cnf.jkt`) via `@dwk/dpop`.
|
|
11
|
+
*/
|
|
12
|
+
import type { AuthContext, ResolvedConfig } from "./config";
|
|
13
|
+
/** A stable reason an authentication attempt failed (for `WWW-Authenticate`). */
|
|
14
|
+
export type AuthFailureReason = "no_jwks" | "token_malformed" | "token_type_invalid" | "signature_invalid" | "issuer_mismatch" | "audience_mismatch" | "token_expired" | "token_not_yet_valid" | "webid_missing" | "cnf_missing" | "dpop_missing" | "dpop_invalid";
|
|
15
|
+
/** Outcome of {@link authenticate}: a context, an explicit failure, or "no creds". */
|
|
16
|
+
export type AuthResult = {
|
|
17
|
+
readonly kind: "authenticated";
|
|
18
|
+
readonly context: AuthContext;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: "anonymous";
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "rejected";
|
|
23
|
+
readonly reason: AuthFailureReason;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Authenticate a request at the edge.
|
|
27
|
+
*
|
|
28
|
+
* Returns `anonymous` when no credentials are presented, `authenticated` with
|
|
29
|
+
* the verified {@link AuthContext} on success, and `rejected` with a stable
|
|
30
|
+
* reason on any failure of a *presented* credential.
|
|
31
|
+
*/
|
|
32
|
+
export declare function authenticate(request: Request, config: ResolvedConfig): Promise<AuthResult>;
|
|
33
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG5D,iFAAiF;AACjF,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,iBAAiB,GACjB,oBAAoB,GACpB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,GACnB,eAAe,GACf,qBAAqB,GACrB,eAAe,GACf,aAAa,GACb,cAAc,GACd,cAAc,CAAC;AAEnB,sFAAsF;AACtF,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GACjE;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAuEtE;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,UAAU,CAAC,CAqFrB"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge authentication for the Resource Server: validate a DPoP-bound bearer
|
|
3
|
+
* token and its proof at the Worker front door, before any request reaches the
|
|
4
|
+
* per-pod Durable Object.
|
|
5
|
+
*
|
|
6
|
+
* A request with no credentials is allowed through unauthenticated — WAC then
|
|
7
|
+
* decides whether the resource is public. A request that *presents* credentials
|
|
8
|
+
* must pass fully or it is rejected `401`: the token signature (issuer JWKS),
|
|
9
|
+
* the `iss` / `aud` / `exp` / `webid` claims, and the RFC 9449 DPoP proof
|
|
10
|
+
* binding (`htu` / `htm` / `ath` / `cnf.jkt`) via `@dwk/dpop`.
|
|
11
|
+
*/
|
|
12
|
+
import { verifyDpopProof } from "@dwk/dpop";
|
|
13
|
+
import { decodeJwt, verifyJwtSignature } from "./jwt";
|
|
14
|
+
const JWKS_TTL_MS = 5 * 60 * 1000;
|
|
15
|
+
const jwksCache = new Map();
|
|
16
|
+
/** Resolve the issuer verification keys: static config first, then cached fetch. */
|
|
17
|
+
async function resolveJwks(config) {
|
|
18
|
+
if (config.jwks && config.jwks.length > 0)
|
|
19
|
+
return config.jwks;
|
|
20
|
+
if (!config.jwksUri)
|
|
21
|
+
return null;
|
|
22
|
+
const now = config.now();
|
|
23
|
+
const cached = jwksCache.get(config.jwksUri);
|
|
24
|
+
if (cached && now - cached.fetchedAt < JWKS_TTL_MS)
|
|
25
|
+
return cached.keys;
|
|
26
|
+
try {
|
|
27
|
+
const response = await config.fetch(config.jwksUri);
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
return cached?.keys ?? null;
|
|
30
|
+
const body = (await response.json());
|
|
31
|
+
// Only cache a well-formed JWKS; caching an empty/garbled body would poison
|
|
32
|
+
// verification for the whole TTL and discard the last good keys.
|
|
33
|
+
if (!Array.isArray(body.keys))
|
|
34
|
+
return cached?.keys ?? null;
|
|
35
|
+
jwksCache.set(config.jwksUri, { keys: body.keys, fetchedAt: now });
|
|
36
|
+
return body.keys;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// A transient fetch failure falls back to the last good keys if we have
|
|
40
|
+
// them, rather than failing closed on every request mid-outage.
|
|
41
|
+
return cached?.keys ?? null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Extract the `DPoP <token>` (or `Bearer <token>`) value, if present. */
|
|
45
|
+
function bearerToken(request) {
|
|
46
|
+
const header = request.headers.get("authorization");
|
|
47
|
+
if (!header)
|
|
48
|
+
return null;
|
|
49
|
+
const match = /^(DPoP|Bearer)\s+(.+)$/i.exec(header.trim());
|
|
50
|
+
return match ? match[2] : null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether the token header's `typ` is the required access-token type. Compared
|
|
54
|
+
* case-insensitively and tolerant of the `application/at+jwt` media-type form,
|
|
55
|
+
* since RFC 9068 permits either the full media type or its `at+jwt` short form.
|
|
56
|
+
*/
|
|
57
|
+
function tokenTypeMatches(typ, required) {
|
|
58
|
+
if (typeof typ !== "string")
|
|
59
|
+
return false;
|
|
60
|
+
const normalize = (value) => {
|
|
61
|
+
const lower = value.toLowerCase();
|
|
62
|
+
return lower.startsWith("application/")
|
|
63
|
+
? lower.slice("application/".length)
|
|
64
|
+
: lower;
|
|
65
|
+
};
|
|
66
|
+
return normalize(typ) === normalize(required);
|
|
67
|
+
}
|
|
68
|
+
/** Whether the token's `aud` claim intersects the accepted audience set. */
|
|
69
|
+
function audienceMatches(aud, accepted) {
|
|
70
|
+
const values = Array.isArray(aud)
|
|
71
|
+
? aud.filter((a) => typeof a === "string")
|
|
72
|
+
: typeof aud === "string"
|
|
73
|
+
? [aud]
|
|
74
|
+
: [];
|
|
75
|
+
return values.some((a) => accepted.includes(a));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Authenticate a request at the edge.
|
|
79
|
+
*
|
|
80
|
+
* Returns `anonymous` when no credentials are presented, `authenticated` with
|
|
81
|
+
* the verified {@link AuthContext} on success, and `rejected` with a stable
|
|
82
|
+
* reason on any failure of a *presented* credential.
|
|
83
|
+
*/
|
|
84
|
+
export async function authenticate(request, config) {
|
|
85
|
+
// A deployer-supplied hook fully replaces the built-in verifier.
|
|
86
|
+
if (config.authenticate) {
|
|
87
|
+
const context = await config.authenticate(request);
|
|
88
|
+
return context ? { kind: "authenticated", context } : { kind: "anonymous" };
|
|
89
|
+
}
|
|
90
|
+
const token = bearerToken(request);
|
|
91
|
+
if (!token)
|
|
92
|
+
return { kind: "anonymous" };
|
|
93
|
+
const decoded = decodeJwt(token);
|
|
94
|
+
if (!decoded)
|
|
95
|
+
return { kind: "rejected", reason: "token_malformed" };
|
|
96
|
+
const jwks = await resolveJwks(config);
|
|
97
|
+
if (!jwks || jwks.length === 0)
|
|
98
|
+
return { kind: "rejected", reason: "no_jwks" };
|
|
99
|
+
if (!(await verifyJwtSignature(decoded, jwks))) {
|
|
100
|
+
return { kind: "rejected", reason: "signature_invalid" };
|
|
101
|
+
}
|
|
102
|
+
// Enforce the access-token `typ` (`at+jwt`) so an ID token or other
|
|
103
|
+
// issuer-signed JWT sharing this `iss`/`aud`/`webid` cannot be replayed as an
|
|
104
|
+
// access token. Skipped when the deployer opts out via `accessTokenType: null`.
|
|
105
|
+
if (config.accessTokenType !== null &&
|
|
106
|
+
!tokenTypeMatches(decoded.header.typ, config.accessTokenType)) {
|
|
107
|
+
return { kind: "rejected", reason: "token_type_invalid" };
|
|
108
|
+
}
|
|
109
|
+
const { iss, aud, exp, nbf, webid, sub, cnf } = decoded.payload;
|
|
110
|
+
if (config.issuer !== undefined && iss !== config.issuer) {
|
|
111
|
+
return { kind: "rejected", reason: "issuer_mismatch" };
|
|
112
|
+
}
|
|
113
|
+
if (!audienceMatches(aud, config.audience)) {
|
|
114
|
+
return { kind: "rejected", reason: "audience_mismatch" };
|
|
115
|
+
}
|
|
116
|
+
const now = Math.floor(config.now() / 1000);
|
|
117
|
+
if (typeof exp !== "number" || now >= exp) {
|
|
118
|
+
return { kind: "rejected", reason: "token_expired" };
|
|
119
|
+
}
|
|
120
|
+
// Honor `nbf` when present: a token is not valid before its not-before time.
|
|
121
|
+
// Per RFC 7519 §4.1.5 `nbf` MUST be a number; a present-but-malformed value is
|
|
122
|
+
// rejected rather than silently bypassed (mirrors the `exp` handling above).
|
|
123
|
+
if (nbf !== undefined && (typeof nbf !== "number" || now < nbf)) {
|
|
124
|
+
return { kind: "rejected", reason: "token_not_yet_valid" };
|
|
125
|
+
}
|
|
126
|
+
// Solid-OIDC carries the agent identity in `webid`; fall back to `sub`.
|
|
127
|
+
const agent = typeof webid === "string"
|
|
128
|
+
? webid
|
|
129
|
+
: typeof sub === "string"
|
|
130
|
+
? sub
|
|
131
|
+
: undefined;
|
|
132
|
+
if (agent === undefined)
|
|
133
|
+
return { kind: "rejected", reason: "webid_missing" };
|
|
134
|
+
const jkt = typeof cnf === "object" &&
|
|
135
|
+
cnf !== null &&
|
|
136
|
+
typeof cnf.jkt === "string"
|
|
137
|
+
? cnf.jkt
|
|
138
|
+
: undefined;
|
|
139
|
+
if (jkt === undefined)
|
|
140
|
+
return { kind: "rejected", reason: "cnf_missing" };
|
|
141
|
+
const proof = request.headers.get("dpop");
|
|
142
|
+
if (!proof)
|
|
143
|
+
return { kind: "rejected", reason: "dpop_missing" };
|
|
144
|
+
const dpop = await verifyDpopProof({
|
|
145
|
+
proof,
|
|
146
|
+
htm: request.method,
|
|
147
|
+
htu: request.url,
|
|
148
|
+
accessToken: token,
|
|
149
|
+
expectedJkt: jkt,
|
|
150
|
+
now,
|
|
151
|
+
});
|
|
152
|
+
if (!dpop.valid || !dpop.jti || !dpop.jkt) {
|
|
153
|
+
return { kind: "rejected", reason: "dpop_invalid" };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
kind: "authenticated",
|
|
157
|
+
context: { webid: agent, jti: dpop.jti, jkt: dpop.jkt },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration, the declared Cloudflare `Env` fragment, and config resolution
|
|
3
|
+
* for `@dwk/solid-pod`.
|
|
4
|
+
*
|
|
5
|
+
* Per the composition contract, the package never reads the global environment
|
|
6
|
+
* directly: all tunables (base URL, token issuer/JWKS, accepted audience,
|
|
7
|
+
* offload threshold, replay/GC windows) are passed into {@link createSolidPod}
|
|
8
|
+
* and {@link createSolidPodGc}, so a pod can be instantiated multiple times and
|
|
9
|
+
* unit-tested in isolation. The Cloudflare bindings — the per-pod Durable
|
|
10
|
+
* Object namespace and the R2 blob bucket — are the only runtime coupling, and
|
|
11
|
+
* a missing one fails loudly at startup.
|
|
12
|
+
*/
|
|
13
|
+
import { type Logger, type Metrics } from "@dwk/log";
|
|
14
|
+
import type { SolidPodObject } from "./pod";
|
|
15
|
+
/** Cloudflare bindings required by the Solid Pod handler and Durable Object. */
|
|
16
|
+
export interface SolidPodEnv {
|
|
17
|
+
/** Durable Object namespace for the per-pod class ({@link SolidPodObject}). */
|
|
18
|
+
readonly POD: DurableObjectNamespace<SolidPodObject>;
|
|
19
|
+
/** R2 bucket holding blob bodies. */
|
|
20
|
+
readonly BLOBS: R2Bucket;
|
|
21
|
+
/**
|
|
22
|
+
* Shared D1 database tracking orphaned blob keys for the out-of-band GC cron.
|
|
23
|
+
* Optional: when bound, the DO opportunistically forwards its transactional
|
|
24
|
+
* orphan outbox here after writes so {@link createSolidPodGc} can reclaim them
|
|
25
|
+
* without ever waking a Durable Object.
|
|
26
|
+
*/
|
|
27
|
+
readonly GC_DB?: D1Database;
|
|
28
|
+
}
|
|
29
|
+
/** Cloudflare bindings required by the out-of-band R2 garbage-collection cron. */
|
|
30
|
+
export interface SolidPodGcEnv {
|
|
31
|
+
/** R2 bucket holding blob bodies. */
|
|
32
|
+
readonly BLOBS: R2Bucket;
|
|
33
|
+
/** Shared D1 database tracking orphaned blob keys (see `@dwk/store`). */
|
|
34
|
+
readonly GC_DB: D1Database;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A verification key set used to validate issuer-signed access tokens. Supply
|
|
38
|
+
* either static {@link SolidPodConfig.jwks} (hermetic, no network) or a
|
|
39
|
+
* {@link SolidPodConfig.jwksUri} the handler fetches and caches.
|
|
40
|
+
*/
|
|
41
|
+
export type Jwks = readonly JsonWebKey[];
|
|
42
|
+
/** Configuration passed to {@link createSolidPod}. */
|
|
43
|
+
export interface SolidPodConfig {
|
|
44
|
+
/**
|
|
45
|
+
* The pod's identity root / base URL, e.g. `https://pod.example`. Used as the
|
|
46
|
+
* origin for absolute resource IRIs in WAC and content negotiation, and as
|
|
47
|
+
* the default token audience. No trailing slash.
|
|
48
|
+
*/
|
|
49
|
+
readonly baseUrl: string;
|
|
50
|
+
/**
|
|
51
|
+
* The pod owner's WebID(s). An owner is always granted full access
|
|
52
|
+
* (`Read`/`Write`/`Append`/`Control`) regardless of ACLs, which bootstraps
|
|
53
|
+
* ACL management — without it, no one could create the first `.acl`.
|
|
54
|
+
*/
|
|
55
|
+
readonly owner?: string | readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* Accepted access-token issuer (`iss`). When set, a token whose `iss` differs
|
|
58
|
+
* is rejected. Required unless a custom {@link SolidPodConfig.authenticate}
|
|
59
|
+
* hook is supplied.
|
|
60
|
+
*/
|
|
61
|
+
readonly issuer?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Accepted token audience(s) (`aud`). A token is accepted when its `aud`
|
|
64
|
+
* intersects this set. Defaults to `["solid", baseUrl]`.
|
|
65
|
+
*/
|
|
66
|
+
readonly audience?: string | readonly string[];
|
|
67
|
+
/**
|
|
68
|
+
* Required access-token header `typ`. Solid-OIDC / RFC 9068 access tokens
|
|
69
|
+
* carry `typ: at+jwt`; enforcing it stops an ID token or other issuer-signed
|
|
70
|
+
* JWT sharing the same `iss`/`aud`/`webid` from being replayed as an access
|
|
71
|
+
* token (token-type confusion). Compared case-insensitively, tolerating the
|
|
72
|
+
* `application/at+jwt` media-type form. Set to `null` to skip the check for
|
|
73
|
+
* issuers that omit `typ`. Defaults to `"at+jwt"`.
|
|
74
|
+
*/
|
|
75
|
+
readonly accessTokenType?: string | null;
|
|
76
|
+
/** Static JWK verification keys for the issuer (hermetic alternative to {@link jwksUri}). */
|
|
77
|
+
readonly jwks?: Jwks;
|
|
78
|
+
/** Issuer JWKS endpoint; fetched and cached when {@link jwks} is not given. */
|
|
79
|
+
readonly jwksUri?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Bodies larger than this (bytes) are offloaded to R2 as opaque blobs instead
|
|
82
|
+
* of the DO SQLite quad store. Defaults to the ~2 MB DO-cell ceiling.
|
|
83
|
+
*/
|
|
84
|
+
readonly maxInlineBytes?: number;
|
|
85
|
+
/**
|
|
86
|
+
* The documented read-replay tradeoff: reads MAY reuse a DPoP proof within
|
|
87
|
+
* this many seconds (edge-cached window) rather than enforcing strict
|
|
88
|
+
* single-use `jti`. Writes are always strict. Defaults to `0` (strict reads).
|
|
89
|
+
*/
|
|
90
|
+
readonly readReplayWindowSeconds?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Allow **unauthenticated** writes (`PUT`/`POST`/`PATCH`/`DELETE`) when WAC
|
|
93
|
+
* grants the public agent class (`acl:agentClass foaf:Agent`) the needed
|
|
94
|
+
* mode. Such requests carry no DPoP proof, so they get **no `jti` replay /
|
|
95
|
+
* anti-abuse protection** — the "DPoP everywhere" guarantee does not hold for
|
|
96
|
+
* them. Defaults to `false`: a tokenless write is refused `401` even where a
|
|
97
|
+
* public-write ACL would otherwise permit it. Set `true` to opt into
|
|
98
|
+
* public write as an explicit, documented tradeoff.
|
|
99
|
+
*/
|
|
100
|
+
readonly allowAnonymousWrites?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* GC safety window (ms) advertised to the cron handler; an orphaned R2 object
|
|
103
|
+
* is only reclaimed once it is older than this. MUST be ≥ the maximum write
|
|
104
|
+
* duration. Defaults to five minutes.
|
|
105
|
+
*/
|
|
106
|
+
readonly gcSafetyWindowMs?: number;
|
|
107
|
+
/** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now`. */
|
|
108
|
+
readonly now?: () => number;
|
|
109
|
+
/** `fetch` implementation used to resolve {@link jwksUri}; defaults to global `fetch`. */
|
|
110
|
+
readonly fetch?: typeof fetch;
|
|
111
|
+
/**
|
|
112
|
+
* Override the edge authentication step entirely. When provided, the handler
|
|
113
|
+
* calls this instead of the built-in JWKS + DPoP validation — useful for
|
|
114
|
+
* tests and for issuers the default verifier does not cover. Returning
|
|
115
|
+
* `null` means "no/invalid credentials" and the request proceeds
|
|
116
|
+
* unauthenticated (WAC then decides public access).
|
|
117
|
+
*/
|
|
118
|
+
readonly authenticate?: (request: Request) => Promise<AuthContext | null> | AuthContext | null;
|
|
119
|
+
/**
|
|
120
|
+
* Logger for auth/authz events; defaults to a no-op. Wired once here at the
|
|
121
|
+
* composition boundary (see `@dwk/log`) to surface edge-authentication
|
|
122
|
+
* rejections and the Durable Object's WAC denials, anonymous-write refusals,
|
|
123
|
+
* and DPoP replay rejections instead of swallowing them.
|
|
124
|
+
*/
|
|
125
|
+
readonly logger?: Logger;
|
|
126
|
+
/**
|
|
127
|
+
* Metrics sink for the same events; defaults to a no-op. Wire an adapter (e.g.
|
|
128
|
+
* `analyticsEngineMetrics` from `@dwk/log`) to chart what the logger names —
|
|
129
|
+
* auth rejections by reason, WAC denials/min, replay rejections.
|
|
130
|
+
*/
|
|
131
|
+
readonly metrics?: Metrics;
|
|
132
|
+
}
|
|
133
|
+
/** The authenticated facts the front door hands to the Durable Object. */
|
|
134
|
+
export interface AuthContext {
|
|
135
|
+
/** The authenticated agent's WebID. */
|
|
136
|
+
readonly webid: string;
|
|
137
|
+
/** The verified DPoP proof's `jti`, for write replay enforcement in the DO. */
|
|
138
|
+
readonly jti: string;
|
|
139
|
+
/** The DPoP key thumbprint the token is bound to (`cnf.jkt`). */
|
|
140
|
+
readonly jkt: string;
|
|
141
|
+
}
|
|
142
|
+
/** Fully-resolved configuration with defaults applied. */
|
|
143
|
+
export interface ResolvedConfig {
|
|
144
|
+
readonly baseUrl: string;
|
|
145
|
+
readonly origin: string;
|
|
146
|
+
readonly owners: readonly string[];
|
|
147
|
+
readonly issuer?: string;
|
|
148
|
+
readonly audience: readonly string[];
|
|
149
|
+
readonly accessTokenType: string | null;
|
|
150
|
+
readonly jwks?: Jwks;
|
|
151
|
+
readonly jwksUri?: string;
|
|
152
|
+
readonly maxInlineBytes?: number;
|
|
153
|
+
readonly readReplayWindowSeconds: number;
|
|
154
|
+
readonly allowAnonymousWrites: boolean;
|
|
155
|
+
readonly gcSafetyWindowMs: number;
|
|
156
|
+
readonly now: () => number;
|
|
157
|
+
readonly fetch: typeof fetch;
|
|
158
|
+
readonly authenticate?: SolidPodConfig["authenticate"];
|
|
159
|
+
readonly logger: Logger;
|
|
160
|
+
readonly metrics: Metrics;
|
|
161
|
+
}
|
|
162
|
+
/** Internal headers the trusted front door uses to hand auth facts to the DO. */
|
|
163
|
+
export declare const INTERNAL_HEADERS: {
|
|
164
|
+
/** Authenticated agent WebID (absent ⇒ unauthenticated request). */
|
|
165
|
+
readonly webid: "x-solid-webid";
|
|
166
|
+
/** Verified DPoP `jti` (present on authenticated writes for replay control). */
|
|
167
|
+
readonly jti: "x-solid-jti";
|
|
168
|
+
/** Verified DPoP key thumbprint (`cnf.jkt`). */
|
|
169
|
+
readonly jkt: "x-solid-jkt";
|
|
170
|
+
/** JSON-encoded subset of config the DO needs (offload threshold, etc.). */
|
|
171
|
+
readonly config: "x-solid-config";
|
|
172
|
+
/**
|
|
173
|
+
* DO→front-door: a machine-readable authorization outcome (see `PodOutcome`)
|
|
174
|
+
* the composition boundary logs via the injected seams, then strips before the
|
|
175
|
+
* response reaches the client.
|
|
176
|
+
*/
|
|
177
|
+
readonly outcome: "x-solid-outcome";
|
|
178
|
+
};
|
|
179
|
+
/** Apply defaults and derived values to raw {@link SolidPodConfig}. */
|
|
180
|
+
export declare function resolveConfig(config: SolidPodConfig): ResolvedConfig;
|
|
181
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration, the declared Cloudflare `Env` fragment, and config resolution
|
|
3
|
+
* for `@dwk/solid-pod`.
|
|
4
|
+
*
|
|
5
|
+
* Per the composition contract, the package never reads the global environment
|
|
6
|
+
* directly: all tunables (base URL, token issuer/JWKS, accepted audience,
|
|
7
|
+
* offload threshold, replay/GC windows) are passed into {@link createSolidPod}
|
|
8
|
+
* and {@link createSolidPodGc}, so a pod can be instantiated multiple times and
|
|
9
|
+
* unit-tested in isolation. The Cloudflare bindings — the per-pod Durable
|
|
10
|
+
* Object namespace and the R2 blob bucket — are the only runtime coupling, and
|
|
11
|
+
* a missing one fails loudly at startup.
|
|
12
|
+
*/
|
|
13
|
+
import { noopLogger, noopMetrics } from "@dwk/log";
|
|
14
|
+
/** Default GC safety window: five minutes, comfortably above any write. */
|
|
15
|
+
const DEFAULT_GC_SAFETY_WINDOW_MS = 5 * 60 * 1000;
|
|
16
|
+
/** Internal headers the trusted front door uses to hand auth facts to the DO. */
|
|
17
|
+
export const INTERNAL_HEADERS = {
|
|
18
|
+
/** Authenticated agent WebID (absent ⇒ unauthenticated request). */
|
|
19
|
+
webid: "x-solid-webid",
|
|
20
|
+
/** Verified DPoP `jti` (present on authenticated writes for replay control). */
|
|
21
|
+
jti: "x-solid-jti",
|
|
22
|
+
/** Verified DPoP key thumbprint (`cnf.jkt`). */
|
|
23
|
+
jkt: "x-solid-jkt",
|
|
24
|
+
/** JSON-encoded subset of config the DO needs (offload threshold, etc.). */
|
|
25
|
+
config: "x-solid-config",
|
|
26
|
+
/**
|
|
27
|
+
* DO→front-door: a machine-readable authorization outcome (see `PodOutcome`)
|
|
28
|
+
* the composition boundary logs via the injected seams, then strips before the
|
|
29
|
+
* response reaches the client.
|
|
30
|
+
*/
|
|
31
|
+
outcome: "x-solid-outcome",
|
|
32
|
+
};
|
|
33
|
+
/** Strip the trailing slash from a base URL so path joins are unambiguous. */
|
|
34
|
+
function normalizeBaseUrl(baseUrl) {
|
|
35
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
36
|
+
}
|
|
37
|
+
/** Apply defaults and derived values to raw {@link SolidPodConfig}. */
|
|
38
|
+
export function resolveConfig(config) {
|
|
39
|
+
if (!config.baseUrl) {
|
|
40
|
+
throw new Error("@dwk/solid-pod: `baseUrl` is required");
|
|
41
|
+
}
|
|
42
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
43
|
+
const origin = new URL(baseUrl).origin;
|
|
44
|
+
const audience = config.audience === undefined
|
|
45
|
+
? ["solid", baseUrl]
|
|
46
|
+
: typeof config.audience === "string"
|
|
47
|
+
? [config.audience]
|
|
48
|
+
: [...config.audience];
|
|
49
|
+
const owners = config.owner === undefined
|
|
50
|
+
? []
|
|
51
|
+
: typeof config.owner === "string"
|
|
52
|
+
? [config.owner]
|
|
53
|
+
: [...config.owner];
|
|
54
|
+
return {
|
|
55
|
+
baseUrl,
|
|
56
|
+
origin,
|
|
57
|
+
owners,
|
|
58
|
+
issuer: config.issuer,
|
|
59
|
+
audience,
|
|
60
|
+
accessTokenType: config.accessTokenType === undefined ? "at+jwt" : config.accessTokenType,
|
|
61
|
+
jwks: config.jwks,
|
|
62
|
+
jwksUri: config.jwksUri,
|
|
63
|
+
maxInlineBytes: config.maxInlineBytes,
|
|
64
|
+
readReplayWindowSeconds: config.readReplayWindowSeconds ?? 0,
|
|
65
|
+
allowAnonymousWrites: config.allowAnonymousWrites ?? false,
|
|
66
|
+
gcSafetyWindowMs: config.gcSafetyWindowMs ?? DEFAULT_GC_SAFETY_WINDOW_MS,
|
|
67
|
+
now: config.now ?? (() => Date.now()),
|
|
68
|
+
fetch: config.fetch ?? fetch,
|
|
69
|
+
authenticate: config.authenticate,
|
|
70
|
+
logger: config.logger ?? noopLogger,
|
|
71
|
+
metrics: config.metrics ?? noopMetrics,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAA6B,MAAM,UAAU,CAAC;AA6K9E,2EAA2E;AAC3E,MAAM,2BAA2B,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAElD,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,oEAAoE;IACpE,KAAK,EAAE,eAAe;IACtB,gFAAgF;IAChF,GAAG,EAAE,aAAa;IAClB,gDAAgD;IAChD,GAAG,EAAE,aAAa;IAClB,4EAA4E;IAC5E,MAAM,EAAE,gBAAgB;IACxB;;;;OAIG;IACH,OAAO,EAAE,iBAAiB;CAClB,CAAC;AAEX,8EAA8E;AAC9E,SAAS,gBAAgB,CAAC,OAAe;IACvC,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAChE,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,MAAsB;IAClD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACvC,MAAM,QAAQ,GACZ,MAAM,CAAC,QAAQ,KAAK,SAAS;QAC3B,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC;QACpB,CAAC,CAAC,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;YACnC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;YACnB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE7B,MAAM,MAAM,GACV,MAAM,CAAC,KAAK,KAAK,SAAS;QACxB,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;YAChC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YAChB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAE1B,OAAO;QACL,OAAO;QACP,MAAM;QACN,MAAM;QACN,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ;QACR,eAAe,EACb,MAAM,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,eAAe;QAC1E,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,uBAAuB,EAAE,MAAM,CAAC,uBAAuB,IAAI,CAAC;QAC5D,oBAAoB,EAAE,MAAM,CAAC,oBAAoB,IAAI,KAAK;QAC1D,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,IAAI,2BAA2B;QACxE,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QACrC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK;QAC5B,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,UAAU;QACnC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,WAAW;KACvC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* base64url + UTF-8 helpers for the edge auth path (JWT/JWKS, DPoP hashing).
|
|
3
|
+
*
|
|
4
|
+
* Dependency-free and runtime-agnostic (Web Crypto / `atob` / `btoa` only) so
|
|
5
|
+
* the surrounding modules unit-test without a Workers runtime.
|
|
6
|
+
*/
|
|
7
|
+
/** Encode bytes as unpadded base64url (RFC 4648 §5). */
|
|
8
|
+
export declare function bytesToBase64url(bytes: Uint8Array): string;
|
|
9
|
+
/** Decode unpadded (or padded) base64url to bytes. */
|
|
10
|
+
export declare function base64urlToBytes(segment: string): Uint8Array;
|
|
11
|
+
/** Decode unpadded base64url to a UTF-8 string. */
|
|
12
|
+
export declare function base64urlToText(segment: string): string;
|
|
13
|
+
//# sourceMappingURL=encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAO1D;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAQ5D;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|
package/dist/encoding.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* base64url + UTF-8 helpers for the edge auth path (JWT/JWKS, DPoP hashing).
|
|
3
|
+
*
|
|
4
|
+
* Dependency-free and runtime-agnostic (Web Crypto / `atob` / `btoa` only) so
|
|
5
|
+
* the surrounding modules unit-test without a Workers runtime.
|
|
6
|
+
*/
|
|
7
|
+
/** Encode bytes as unpadded base64url (RFC 4648 §5). */
|
|
8
|
+
export function bytesToBase64url(bytes) {
|
|
9
|
+
let binary = "";
|
|
10
|
+
for (const byte of bytes)
|
|
11
|
+
binary += String.fromCharCode(byte);
|
|
12
|
+
return btoa(binary)
|
|
13
|
+
.replace(/\+/g, "-")
|
|
14
|
+
.replace(/\//g, "_")
|
|
15
|
+
.replace(/=+$/, "");
|
|
16
|
+
}
|
|
17
|
+
/** Decode unpadded (or padded) base64url to bytes. */
|
|
18
|
+
export function base64urlToBytes(segment) {
|
|
19
|
+
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
20
|
+
const padded = b64.length % 4 === 0 ? b64 : b64 + "=".repeat(4 - (b64.length % 4));
|
|
21
|
+
const binary = atob(padded);
|
|
22
|
+
const bytes = new Uint8Array(binary.length);
|
|
23
|
+
for (let i = 0; i < binary.length; i++)
|
|
24
|
+
bytes[i] = binary.charCodeAt(i);
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
/** Decode unpadded base64url to a UTF-8 string. */
|
|
28
|
+
export function base64urlToText(segment) {
|
|
29
|
+
return new TextDecoder().decode(base64urlToBytes(segment));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.js","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wDAAwD;AACxD,MAAM,UAAU,gBAAgB,CAAC,KAAiB;IAChD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC;SAChB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACxB,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,MAAM,GACV,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACtE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACxE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;AAC7D,CAAC"}
|