@dwk/remotestorage 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.
Files changed (68) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +131 -0
  3. package/dist/auth.d.ts +42 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +108 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/config.d.ts +132 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +67 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/cors.d.ts +23 -0
  12. package/dist/cors.d.ts.map +1 -0
  13. package/dist/cors.js +56 -0
  14. package/dist/cors.js.map +1 -0
  15. package/dist/discovery.d.ts +31 -0
  16. package/dist/discovery.d.ts.map +1 -0
  17. package/dist/discovery.js +35 -0
  18. package/dist/discovery.js.map +1 -0
  19. package/dist/encoding.d.ts +11 -0
  20. package/dist/encoding.d.ts.map +1 -0
  21. package/dist/encoding.js +21 -0
  22. package/dist/encoding.js.map +1 -0
  23. package/dist/folder.d.ts +78 -0
  24. package/dist/folder.d.ts.map +1 -0
  25. package/dist/folder.js +0 -0
  26. package/dist/folder.js.map +1 -0
  27. package/dist/gc.d.ts +23 -0
  28. package/dist/gc.d.ts.map +1 -0
  29. package/dist/gc.js +34 -0
  30. package/dist/gc.js.map +1 -0
  31. package/dist/handler.d.ts +21 -0
  32. package/dist/handler.d.ts.map +1 -0
  33. package/dist/handler.js +166 -0
  34. package/dist/handler.js.map +1 -0
  35. package/dist/index.d.ts +33 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +32 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/jwt.d.ts +35 -0
  40. package/dist/jwt.d.ts.map +1 -0
  41. package/dist/jwt.js +119 -0
  42. package/dist/jwt.js.map +1 -0
  43. package/dist/log.d.ts +29 -0
  44. package/dist/log.d.ts.map +1 -0
  45. package/dist/log.js +27 -0
  46. package/dist/log.js.map +1 -0
  47. package/dist/scope.d.ts +54 -0
  48. package/dist/scope.d.ts.map +1 -0
  49. package/dist/scope.js +83 -0
  50. package/dist/scope.js.map +1 -0
  51. package/dist/storage.d.ts +22 -0
  52. package/dist/storage.d.ts.map +1 -0
  53. package/dist/storage.js +313 -0
  54. package/dist/storage.js.map +1 -0
  55. package/package.json +50 -0
  56. package/src/auth.ts +146 -0
  57. package/src/config.ts +197 -0
  58. package/src/cors.ts +68 -0
  59. package/src/discovery.ts +48 -0
  60. package/src/encoding.ts +22 -0
  61. package/src/folder.ts +0 -0
  62. package/src/gc.ts +50 -0
  63. package/src/handler.ts +225 -0
  64. package/src/index.ts +84 -0
  65. package/src/jwt.ts +155 -0
  66. package/src/log.ts +31 -0
  67. package/src/scope.ts +99 -0
  68. package/src/storage.ts +398 -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,131 @@
1
+ # `@dwk/remotestorage`
2
+
3
+ > remoteStorage (draft-dejong-remotestorage) personal data vault. Endpoint
4
+ > package + per-account Durable Object.
5
+
6
+ Part of the [`@dwk` IndieWeb + Solid cohort](../../README.md). See the
7
+ [package specification](../../spec/packages/remotestorage.md) for the full
8
+ requirements.
9
+
10
+ An [Unhosted](https://unhosted.org/)-style **remoteStorage** server: a per-user
11
+ document vault that "no-backend" web apps read and write over a plain HTTP
12
+ `GET`/`PUT`/`DELETE` API, scoped by OAuth 2.0 bearer tokens. It is a *competing*
13
+ personal-data protocol to Solid — simpler, document-oriented, **no RDF** — and
14
+ is filed here for completeness, **not** as a recommendation alongside
15
+ [`@dwk/solid-pod`](../solid-pod).
16
+
17
+ ## Why it's here: one backing store, two protocols
18
+
19
+ [`@dwk/store`](../store) is **not** Solid-specific — it is a generic
20
+ `key → { rdf | blob }` pointer map over DO-SQLite + content-addressed,
21
+ copy-on-write R2 bodies with TOCTOU-free conditional writes and an orphan-outbox
22
+ GC. remoteStorage documents are simply its **blob tier**, so a remoteStorage
23
+ vault and a Solid Pod can ride the same library, the same R2 bucket, and the same
24
+ GC. The only library addition this package required is a single, **generic**
25
+ projection on the `Store` interface:
26
+
27
+ ```ts
28
+ store.list(prefix); // every resource pointer whose key starts with `prefix`
29
+ ```
30
+
31
+ `list(prefix)` ascribes no meaning to `/` or to "folders" — the folder model and
32
+ its aggregate ETags are derived in this package — so `@dwk/solid-pod` could use
33
+ the same projection to enumerate LDP container membership. Everything that makes
34
+ remoteStorage *remoteStorage* (the auth model and folder semantics) lives here.
35
+
36
+ ## Usage
37
+
38
+ ```ts
39
+ import {
40
+ createRemoteStorage,
41
+ createRemoteStorageGc,
42
+ RemoteStorageObject,
43
+ } from "@dwk/remotestorage";
44
+
45
+ const storage = createRemoteStorage({
46
+ baseUrl: "https://storage.example",
47
+ // Built-in OAuth bearer (JWT) verification against the issuer's JWKS:
48
+ issuer: "https://auth.example",
49
+ jwksUri: "https://auth.example/jwks",
50
+ // …or pass `authenticate(request)` to resolve opaque tokens (e.g. RFC 7662
51
+ // introspection via @dwk/oauth) into `{ scopes }`.
52
+ });
53
+
54
+ // In your Worker, route the storage tree (default: `/<account>/<path…>`):
55
+ // PUT /alice/documents/note.txt
56
+ // GET /alice/documents/
57
+ // GET /alice/public/photos/cat.jpg (no token needed)
58
+ export default { fetch: storage };
59
+ export { RemoteStorageObject };
60
+
61
+ // Bind a cron trigger to reclaim orphaned R2 bodies:
62
+ export const scheduled = createRemoteStorageGc({ baseUrl: "https://storage.example" });
63
+ ```
64
+
65
+ **Bindings** (declared `Env` fragment, fail-loud if missing): a Durable Object
66
+ namespace `STORAGE` for the per-account class, an R2 bucket `BLOBS` (MAY be
67
+ shared with `@dwk/solid-pod`), and an optional D1 `GC_DB` the DO forwards orphan
68
+ keys into for the GC cron.
69
+
70
+ ### Documents
71
+
72
+ - `PUT` a document → records its `Content-Type`, returns the new strong `ETag`
73
+ (`201` on create, `200` on overwrite). Honors `If-Match` and `If-None-Match: *`
74
+ (create-only), checked TOCTOU-free inside the store's write transaction (`412`
75
+ on failure). Oversized bodies stream straight to R2, never buffered in the DO.
76
+ - `GET`/`HEAD` a document → body + `Content-Type` + `ETag`; `If-None-Match`
77
+ yields `304`.
78
+ - `DELETE` a document → `200` with the deleted document's `ETag`; emptied parent
79
+ folders simply vanish (they are virtual).
80
+ - A name that is already a folder (or whose ancestor is already a document) is a
81
+ collision → **409**. A `PUT`/`DELETE` on a folder path (trailing slash) → **400**.
82
+
83
+ ### Folders
84
+
85
+ `GET <path>/` returns the `application/ld+json`
86
+ [folder description](https://datatracker.ietf.org/doc/html/draft-dejong-remotestorage-22)
87
+ (`http://remotestorage.io/spec/folder-description`): a map of immediate children
88
+ (documents with their `ETag` + `Content-Type`, subfolders with an aggregate
89
+ `ETag`) and a folder `ETag`. The folder ETag is a SHA-256 over a canonical
90
+ signature of **every descendant**, so it changes whenever anything in the subtree
91
+ does. An empty/absent folder still answers `200` with a stable ETag.
92
+
93
+ ### Authorization (scopes)
94
+
95
+ Plain OAuth 2.0 **bearer** tokens (no DPoP). A token's `scope` claim is a
96
+ space-delimited list of `<module>:r` / `<module>:rw` entries, where the module is
97
+ a top-level folder name or `*` (the whole vault). A module scope covers both the
98
+ private tree `/<module>/…` and the public tree `/public/<module>/…`.
99
+
100
+ - **Reads** of `/public/` **documents** need no token (folders are never public).
101
+ - Every other access needs a scope at the required mode; a write without `rw` →
102
+ **403**, a missing/invalid token on a private path → **401**.
103
+
104
+ ### CORS
105
+
106
+ Permissive CORS on every response (draft §6) so browser apps on other origins
107
+ work: `OPTIONS` preflights are answered at the edge, and `ETag`/`Content-Type`/
108
+ `Content-Length` are exposed. Bearer tokens travel in `Authorization`, so no
109
+ credentialed CORS is needed.
110
+
111
+ ### Discovery
112
+
113
+ `remoteStorageLink({ storageRoot, authEndpoint })` builds the WebFinger link a
114
+ connecting app reads to find a user's storage root and OAuth endpoint — drop it
115
+ into a [`@dwk/webfinger`](../webfinger) resource record.
116
+
117
+ ## Design
118
+
119
+ Stateless front door (CORS + token verification + scope enforcement) over a
120
+ per-account Durable Object that is the single consistency authority (DO-SQLite +
121
+ R2 via `@dwk/store`). Authoritative state lives only in strongly-consistent
122
+ stores — **never KV**. The scope, folder, and CORS logic is pure and unit-tests
123
+ without a Workers runtime.
124
+
125
+ ## Observability
126
+
127
+ Auth/authz events flow through the injected `@dwk/log` `Logger`/`Metrics` seams
128
+ (default no-op): `remotestorage.auth.accepted` / `.rejected` and
129
+ `remotestorage.scope.denied`. Fields are redacted to reason codes, HTTP
130
+ method/status, and a sanitized subject host — never tokens, scopes verbatim,
131
+ paths, or bodies.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Edge authentication for the vault: resolve an OAuth 2.0 bearer token into the
3
+ * scopes it grants, before any request reaches the per-account Durable Object.
4
+ *
5
+ * remoteStorage uses **plain bearer tokens** (no DPoP). A request with no
6
+ * `Authorization` header is `anonymous` — only `/public/` document reads will
7
+ * then succeed. A request that *presents* a token must verify fully or it is
8
+ * `rejected`: the built-in verifier checks the issuer JWKS signature and the
9
+ * `iss` / `aud` / `exp` / `nbf` claims, then reads the OAuth `scope` claim. A
10
+ * deployer-supplied {@link RemoteStorageConfig.authenticate} hook fully replaces
11
+ * the built-in path (e.g. to introspect opaque tokens via RFC 7662).
12
+ */
13
+ import type { ResolvedConfig } from "./config";
14
+ import { type RemoteStorageScope } from "./scope";
15
+ /** The verified facts a token yields: its scopes and (optionally) its subject. */
16
+ export interface RemoteStorageAuth {
17
+ /** The remoteStorage scopes the token grants. */
18
+ readonly scopes: readonly RemoteStorageScope[];
19
+ /** The token subject (`sub`), used only for sanitized logging. */
20
+ readonly subject?: string;
21
+ }
22
+ /** A stable reason a token failed verification (for `WWW-Authenticate`). */
23
+ export type AuthFailureReason = "no_jwks" | "token_malformed" | "signature_invalid" | "issuer_mismatch" | "audience_mismatch" | "token_expired" | "token_not_yet_valid";
24
+ /** Outcome of {@link authenticate}: scopes, an explicit failure, or "no creds". */
25
+ export type AuthResult = {
26
+ readonly kind: "authenticated";
27
+ readonly auth: RemoteStorageAuth;
28
+ } | {
29
+ readonly kind: "anonymous";
30
+ } | {
31
+ readonly kind: "rejected";
32
+ readonly reason: AuthFailureReason;
33
+ };
34
+ /** Extract the `Bearer <token>` value, if present. */
35
+ export declare function bearerToken(request: Request): string | null;
36
+ /**
37
+ * Authenticate a request at the edge. Returns `anonymous` when no credentials
38
+ * are presented, `authenticated` with the granted scopes on success, and
39
+ * `rejected` with a stable reason on any failure of a *presented* token.
40
+ */
41
+ export declare function authenticate(request: Request, config: ResolvedConfig): Promise<AuthResult>;
42
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE/D,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC/C,kEAAkE;IAClE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,iBAAiB,GACjB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,GACnB,eAAe,GACf,qBAAqB,CAAC;AAE1B,mFAAmF;AACnF,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GACpE;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;AAoCtE,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK3D;AAaD;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,UAAU,CAAC,CA2CrB"}
package/dist/auth.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Edge authentication for the vault: resolve an OAuth 2.0 bearer token into the
3
+ * scopes it grants, before any request reaches the per-account Durable Object.
4
+ *
5
+ * remoteStorage uses **plain bearer tokens** (no DPoP). A request with no
6
+ * `Authorization` header is `anonymous` — only `/public/` document reads will
7
+ * then succeed. A request that *presents* a token must verify fully or it is
8
+ * `rejected`: the built-in verifier checks the issuer JWKS signature and the
9
+ * `iss` / `aud` / `exp` / `nbf` claims, then reads the OAuth `scope` claim. A
10
+ * deployer-supplied {@link RemoteStorageConfig.authenticate} hook fully replaces
11
+ * the built-in path (e.g. to introspect opaque tokens via RFC 7662).
12
+ */
13
+ import { decodeJwt, verifyJwtSignature } from "./jwt";
14
+ import { parseScopes } from "./scope";
15
+ const JWKS_TTL_MS = 5 * 60 * 1000;
16
+ const jwksCache = new Map();
17
+ /** Resolve the issuer verification keys: static config first, then cached fetch. */
18
+ async function resolveJwks(config) {
19
+ if (config.jwks && config.jwks.length > 0)
20
+ return config.jwks;
21
+ if (!config.jwksUri)
22
+ return null;
23
+ const now = config.now();
24
+ const cached = jwksCache.get(config.jwksUri);
25
+ if (cached && now - cached.fetchedAt < JWKS_TTL_MS)
26
+ return cached.keys;
27
+ try {
28
+ const response = await config.fetch(config.jwksUri);
29
+ if (!response.ok)
30
+ return cached?.keys ?? null;
31
+ const body = (await response.json());
32
+ // Only cache a well-formed JWKS; a null/empty/garbled body (e.g. literal
33
+ // JSON `null`) is ignored rather than throwing — and caching it would poison
34
+ // verification for the whole TTL and discard the last good keys.
35
+ if (!body || !Array.isArray(body.keys))
36
+ return cached?.keys ?? null;
37
+ jwksCache.set(config.jwksUri, { keys: body.keys, fetchedAt: now });
38
+ return body.keys;
39
+ }
40
+ catch {
41
+ return cached?.keys ?? null;
42
+ }
43
+ }
44
+ /** Extract the `Bearer <token>` value, if present. */
45
+ export function bearerToken(request) {
46
+ const header = request.headers.get("authorization");
47
+ if (!header)
48
+ return null;
49
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
50
+ return match ? match[1] : null;
51
+ }
52
+ /** Whether the token's `aud` claim intersects the accepted audience set. */
53
+ function audienceMatches(aud, accepted) {
54
+ if (accepted.length === 0)
55
+ return true; // audience check disabled
56
+ const values = Array.isArray(aud)
57
+ ? aud.filter((a) => typeof a === "string")
58
+ : typeof aud === "string"
59
+ ? [aud]
60
+ : [];
61
+ return values.some((a) => accepted.includes(a));
62
+ }
63
+ /**
64
+ * Authenticate a request at the edge. Returns `anonymous` when no credentials
65
+ * are presented, `authenticated` with the granted scopes on success, and
66
+ * `rejected` with a stable reason on any failure of a *presented* token.
67
+ */
68
+ export async function authenticate(request, config) {
69
+ // A deployer-supplied hook fully replaces the built-in verifier.
70
+ if (config.authenticate) {
71
+ const auth = await config.authenticate(request);
72
+ return auth ? { kind: "authenticated", auth } : { kind: "anonymous" };
73
+ }
74
+ const token = bearerToken(request);
75
+ if (!token)
76
+ return { kind: "anonymous" };
77
+ const decoded = decodeJwt(token);
78
+ if (!decoded)
79
+ return { kind: "rejected", reason: "token_malformed" };
80
+ const jwks = await resolveJwks(config);
81
+ if (!jwks || jwks.length === 0)
82
+ return { kind: "rejected", reason: "no_jwks" };
83
+ if (!(await verifyJwtSignature(decoded, jwks))) {
84
+ return { kind: "rejected", reason: "signature_invalid" };
85
+ }
86
+ const { iss, aud, exp, nbf, sub, scope } = decoded.payload;
87
+ if (config.issuer !== undefined && iss !== config.issuer) {
88
+ return { kind: "rejected", reason: "issuer_mismatch" };
89
+ }
90
+ if (!audienceMatches(aud, config.audience)) {
91
+ return { kind: "rejected", reason: "audience_mismatch" };
92
+ }
93
+ const now = Math.floor(config.now() / 1000);
94
+ if (typeof exp !== "number" || now >= exp) {
95
+ return { kind: "rejected", reason: "token_expired" };
96
+ }
97
+ if (nbf !== undefined && (typeof nbf !== "number" || now < nbf)) {
98
+ return { kind: "rejected", reason: "token_not_yet_valid" };
99
+ }
100
+ return {
101
+ kind: "authenticated",
102
+ auth: {
103
+ scopes: parseScopes(typeof scope === "string" ? scope : undefined),
104
+ ...(typeof sub === "string" ? { subject: sub } : {}),
105
+ },
106
+ };
107
+ }
108
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Configuration, the declared Cloudflare `Env` fragment, and config resolution
3
+ * for `@dwk/remotestorage`.
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, GC window, path mapping) are passed into
8
+ * {@link createRemoteStorage} / {@link createRemoteStorageGc}, so a vault can be
9
+ * instantiated multiple times and unit-tested in isolation. The Cloudflare
10
+ * bindings — the per-account Durable Object namespace and the R2 blob bucket —
11
+ * are the only runtime coupling, and a missing one fails loudly at startup.
12
+ */
13
+ import { type Logger, type Metrics } from "@dwk/log";
14
+ import type { RemoteStorageAuth } from "./auth";
15
+ import type { RemoteStorageObject } from "./storage";
16
+ /** Cloudflare bindings required by the handler and the per-account Durable Object. */
17
+ export interface RemoteStorageEnv {
18
+ /** Durable Object namespace for the per-account class ({@link RemoteStorageObject}). */
19
+ readonly STORAGE: DurableObjectNamespace<RemoteStorageObject>;
20
+ /** R2 bucket holding document bodies (MAY be shared with `@dwk/solid-pod`). */
21
+ readonly BLOBS: R2Bucket;
22
+ /**
23
+ * Shared D1 database tracking orphaned blob keys for the out-of-band GC cron.
24
+ * Optional: when bound, the DO forwards its transactional orphan outbox here
25
+ * after writes so {@link createRemoteStorageGc} can reclaim them without ever
26
+ * waking a Durable Object.
27
+ */
28
+ readonly GC_DB?: D1Database;
29
+ }
30
+ /** Cloudflare bindings required by the out-of-band R2 garbage-collection cron. */
31
+ export interface RemoteStorageGcEnv {
32
+ /** R2 bucket holding document bodies. */
33
+ readonly BLOBS: R2Bucket;
34
+ /** Shared D1 database tracking orphaned blob keys (see `@dwk/store`). */
35
+ readonly GC_DB: D1Database;
36
+ }
37
+ /** The account and account-relative storage path parsed from a request URL. */
38
+ export interface ParsedPath {
39
+ /** Stable account identifier; keys the per-account Durable Object. */
40
+ readonly account: string;
41
+ /** Account-relative storage path, percent-encoded, always starting with `/`. */
42
+ readonly path: string;
43
+ }
44
+ /** Configuration passed to {@link createRemoteStorage}. */
45
+ export interface RemoteStorageConfig {
46
+ /**
47
+ * The storage server's base URL, e.g. `https://storage.example`. Used for
48
+ * discovery metadata and as a default token audience. No trailing slash.
49
+ */
50
+ readonly baseUrl: string;
51
+ /**
52
+ * Accepted access-token issuer (`iss`). When set, a token whose `iss` differs
53
+ * is rejected. Ignored when a custom {@link authenticate} hook is supplied.
54
+ */
55
+ readonly issuer?: string;
56
+ /**
57
+ * Accepted token audience(s) (`aud`). When set, a token is accepted only when
58
+ * its `aud` intersects this set; when unset the audience check is skipped
59
+ * (plain OAuth bearer tokens often omit `aud`).
60
+ */
61
+ readonly audience?: string | readonly string[];
62
+ /** Static JWK verification keys for the issuer (hermetic alternative to {@link jwksUri}). */
63
+ readonly jwks?: readonly JsonWebKey[];
64
+ /** Issuer JWKS endpoint; fetched and cached when {@link jwks} is not given. */
65
+ readonly jwksUri?: string;
66
+ /**
67
+ * Bodies larger than this (bytes) are offloaded to R2 as opaque blobs instead
68
+ * of the DO SQLite cell. Defaults to the ~2 MB DO-cell ceiling in `@dwk/store`.
69
+ */
70
+ readonly maxInlineBytes?: number;
71
+ /**
72
+ * GC safety window (ms) advertised to the cron handler; an orphaned R2 object
73
+ * is reclaimed only once it is older than this. MUST be ≥ the maximum write
74
+ * duration. Defaults to five minutes.
75
+ */
76
+ readonly gcSafetyWindowMs?: number;
77
+ /** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now`. */
78
+ readonly now?: () => number;
79
+ /** `fetch` implementation used to resolve {@link jwksUri}; defaults to global `fetch`. */
80
+ readonly fetch?: typeof fetch;
81
+ /**
82
+ * Override the bearer-token verification entirely. When provided, the handler
83
+ * calls this instead of the built-in JWKS verifier — useful for tests and for
84
+ * token formats the default verifier does not cover (e.g. opaque tokens
85
+ * resolved via RFC 7662 introspection). Returning `null` means "no/invalid
86
+ * credentials"; the request then proceeds unauthenticated (only `/public/`
87
+ * document reads succeed).
88
+ */
89
+ readonly authenticate?: (request: Request) => Promise<RemoteStorageAuth | null> | RemoteStorageAuth | null;
90
+ /**
91
+ * Map a request `pathname` to an {@link ParsedPath}. Defaults to treating the
92
+ * first path segment as the account and the remainder (with leading slash) as
93
+ * the account-relative storage path: `/alice/documents/x` → account `alice`,
94
+ * path `/documents/x`; `/alice` or `/alice/` → account `alice`, path `/`.
95
+ * Return `null` to 404 a request that names no account.
96
+ */
97
+ readonly parsePath?: (pathname: string) => ParsedPath | null;
98
+ /** Logger for auth/authz events; defaults to a no-op (see `@dwk/log`). */
99
+ readonly logger?: Logger;
100
+ /** Metrics sink for the same events; defaults to a no-op. */
101
+ readonly metrics?: Metrics;
102
+ }
103
+ /** Fully-resolved configuration with defaults applied. */
104
+ export interface ResolvedConfig {
105
+ readonly baseUrl: string;
106
+ readonly issuer?: string;
107
+ readonly audience: readonly string[];
108
+ readonly jwks?: readonly JsonWebKey[];
109
+ readonly jwksUri?: string;
110
+ readonly maxInlineBytes?: number;
111
+ readonly gcSafetyWindowMs: number;
112
+ readonly now: () => number;
113
+ readonly fetch: typeof fetch;
114
+ readonly authenticate?: RemoteStorageConfig["authenticate"];
115
+ readonly parsePath: (pathname: string) => ParsedPath | null;
116
+ readonly logger: Logger;
117
+ readonly metrics: Metrics;
118
+ }
119
+ /** Internal header the trusted front door uses to hand config to the DO. */
120
+ export declare const INTERNAL_HEADERS: {
121
+ /** JSON-encoded subset of config the DO needs (offload threshold). */
122
+ readonly config: "x-remotestorage-config";
123
+ };
124
+ /**
125
+ * Default {@link RemoteStorageConfig.parsePath}: first segment is the account,
126
+ * the rest (with leading slash) is the storage path. Returns `null` when the
127
+ * path names no account (e.g. `/`).
128
+ */
129
+ export declare function defaultParsePath(pathname: string): ParsedPath | null;
130
+ /** Apply defaults and derived values to raw {@link RemoteStorageConfig}. */
131
+ export declare function resolveConfig(config: RemoteStorageConfig): ResolvedConfig;
132
+ //# 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,67 @@
1
+ /**
2
+ * Configuration, the declared Cloudflare `Env` fragment, and config resolution
3
+ * for `@dwk/remotestorage`.
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, GC window, path mapping) are passed into
8
+ * {@link createRemoteStorage} / {@link createRemoteStorageGc}, so a vault can be
9
+ * instantiated multiple times and unit-tested in isolation. The Cloudflare
10
+ * bindings — the per-account Durable Object namespace and the R2 blob bucket —
11
+ * are the only runtime coupling, and 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 header the trusted front door uses to hand config to the DO. */
17
+ export const INTERNAL_HEADERS = {
18
+ /** JSON-encoded subset of config the DO needs (offload threshold). */
19
+ config: "x-remotestorage-config",
20
+ };
21
+ /** Strip the trailing slash from a base URL so joins are unambiguous. */
22
+ function normalizeBaseUrl(baseUrl) {
23
+ return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
24
+ }
25
+ /**
26
+ * Default {@link RemoteStorageConfig.parsePath}: first segment is the account,
27
+ * the rest (with leading slash) is the storage path. Returns `null` when the
28
+ * path names no account (e.g. `/`).
29
+ */
30
+ export function defaultParsePath(pathname) {
31
+ const match = /^\/([^/]+)(\/.*)?$/.exec(pathname);
32
+ if (!match)
33
+ return null;
34
+ const account = decodeURIComponent(match[1]);
35
+ // Keep the storage path percent-encoded: decoding `%2F` would conflate it
36
+ // with a real separator and corrupt store keys.
37
+ const path = match[2] ?? "/";
38
+ return { account, path };
39
+ }
40
+ /** Apply defaults and derived values to raw {@link RemoteStorageConfig}. */
41
+ export function resolveConfig(config) {
42
+ if (!config.baseUrl) {
43
+ throw new Error("@dwk/remotestorage: `baseUrl` is required");
44
+ }
45
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
46
+ const audience = config.audience === undefined
47
+ ? []
48
+ : typeof config.audience === "string"
49
+ ? [config.audience]
50
+ : [...config.audience];
51
+ return {
52
+ baseUrl,
53
+ issuer: config.issuer,
54
+ audience,
55
+ jwks: config.jwks,
56
+ jwksUri: config.jwksUri,
57
+ maxInlineBytes: config.maxInlineBytes,
58
+ gcSafetyWindowMs: config.gcSafetyWindowMs ?? DEFAULT_GC_SAFETY_WINDOW_MS,
59
+ now: config.now ?? (() => Date.now()),
60
+ fetch: config.fetch ?? fetch,
61
+ authenticate: config.authenticate,
62
+ parsePath: config.parsePath ?? defaultParsePath,
63
+ logger: config.logger ?? noopLogger,
64
+ metrics: config.metrics ?? noopMetrics,
65
+ };
66
+ }
67
+ //# 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;AA8H9E,2EAA2E;AAC3E,MAAM,2BAA2B,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAElD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,sEAAsE;IACtE,MAAM,EAAE,wBAAwB;CACxB,CAAC;AAEX,yEAAyE;AACzE,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;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC,CAAC;IACvD,0EAA0E;IAC1E,gDAAgD;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;IAC7B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,MAA2B;IACvD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,QAAQ,GACZ,MAAM,CAAC,QAAQ,KAAK,SAAS;QAC3B,CAAC,CAAC,EAAE;QACJ,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,OAAO;QACL,OAAO;QACP,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ;QACR,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,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,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,gBAAgB;QAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,UAAU;QACnC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,WAAW;KACvC,CAAC;AACJ,CAAC"}
package/dist/cors.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Permissive CORS for browser apps on other origins (draft §6). remoteStorage
3
+ * is built for "no-backend" web apps that run on an origin different from the
4
+ * storage server, so every response — including errors and the preflight — must
5
+ * carry CORS headers or the browser hides the result from the app.
6
+ *
7
+ * Pure and runtime-free: it maps a request `Origin` to the header set. Bearer
8
+ * tokens travel in the `Authorization` header (not cookies), so credentialed
9
+ * CORS is unnecessary and `*` is a safe default; when a concrete `Origin` is
10
+ * present it is echoed (with `Vary: Origin`) so caches stay correct.
11
+ */
12
+ /** Methods the storage API exposes, advertised on the preflight. */
13
+ export declare const ALLOWED_METHODS = "GET, HEAD, PUT, DELETE, OPTIONS";
14
+ /** Build the CORS header set for a request with the given `Origin` (may be null). */
15
+ export declare function corsHeaders(requestOrigin: string | null): Record<string, string>;
16
+ /** The preflight (`OPTIONS`) response, answered at the edge without a store hit. */
17
+ export declare function preflightResponse(requestOrigin: string | null): Response;
18
+ /**
19
+ * Return `response` with the CORS headers merged in. The body and status are
20
+ * preserved; existing headers win only for names CORS does not own.
21
+ */
22
+ export declare function withCors(response: Response, requestOrigin: string | null): Response;
23
+ //# sourceMappingURL=cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oEAAoE;AACpE,eAAO,MAAM,eAAe,oCAAoC,CAAC;AAYjE,qFAAqF;AACrF,wBAAgB,WAAW,CACzB,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAUxE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,QAAQ,CAUV"}
package/dist/cors.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Permissive CORS for browser apps on other origins (draft §6). remoteStorage
3
+ * is built for "no-backend" web apps that run on an origin different from the
4
+ * storage server, so every response — including errors and the preflight — must
5
+ * carry CORS headers or the browser hides the result from the app.
6
+ *
7
+ * Pure and runtime-free: it maps a request `Origin` to the header set. Bearer
8
+ * tokens travel in the `Authorization` header (not cookies), so credentialed
9
+ * CORS is unnecessary and `*` is a safe default; when a concrete `Origin` is
10
+ * present it is echoed (with `Vary: Origin`) so caches stay correct.
11
+ */
12
+ /** Methods the storage API exposes, advertised on the preflight. */
13
+ export const ALLOWED_METHODS = "GET, HEAD, PUT, DELETE, OPTIONS";
14
+ /** Request headers a browser app may send (preflight `Access-Control-Allow-Headers`). */
15
+ const ALLOWED_HEADERS = "Authorization, Content-Type, Content-Length, If-Match, If-None-Match, Origin, X-Requested-With";
16
+ /** Response headers a browser app may read (`Access-Control-Expose-Headers`). */
17
+ const EXPOSED_HEADERS = "ETag, Content-Type, Content-Length, Location";
18
+ /** How long (seconds) a browser may cache the preflight result. */
19
+ const MAX_AGE = "86400";
20
+ /** Build the CORS header set for a request with the given `Origin` (may be null). */
21
+ export function corsHeaders(requestOrigin) {
22
+ const headers = {
23
+ "access-control-allow-origin": requestOrigin ?? "*",
24
+ "access-control-expose-headers": EXPOSED_HEADERS,
25
+ vary: "Origin",
26
+ };
27
+ return headers;
28
+ }
29
+ /** The preflight (`OPTIONS`) response, answered at the edge without a store hit. */
30
+ export function preflightResponse(requestOrigin) {
31
+ return new Response(null, {
32
+ status: 204,
33
+ headers: {
34
+ ...corsHeaders(requestOrigin),
35
+ "access-control-allow-methods": ALLOWED_METHODS,
36
+ "access-control-allow-headers": ALLOWED_HEADERS,
37
+ "access-control-max-age": MAX_AGE,
38
+ },
39
+ });
40
+ }
41
+ /**
42
+ * Return `response` with the CORS headers merged in. The body and status are
43
+ * preserved; existing headers win only for names CORS does not own.
44
+ */
45
+ export function withCors(response, requestOrigin) {
46
+ const headers = new Headers(response.headers);
47
+ for (const [name, value] of Object.entries(corsHeaders(requestOrigin))) {
48
+ headers.set(name, value);
49
+ }
50
+ return new Response(response.body, {
51
+ status: response.status,
52
+ statusText: response.statusText,
53
+ headers,
54
+ });
55
+ }
56
+ //# sourceMappingURL=cors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.js","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oEAAoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG,iCAAiC,CAAC;AAEjE,yFAAyF;AACzF,MAAM,eAAe,GACnB,gGAAgG,CAAC;AAEnG,iFAAiF;AACjF,MAAM,eAAe,GAAG,8CAA8C,CAAC;AAEvE,mEAAmE;AACnE,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB,qFAAqF;AACrF,MAAM,UAAU,WAAW,CACzB,aAA4B;IAE5B,MAAM,OAAO,GAA2B;QACtC,6BAA6B,EAAE,aAAa,IAAI,GAAG;QACnD,+BAA+B,EAAE,eAAe;QAChD,IAAI,EAAE,QAAQ;KACf,CAAC;IACF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,iBAAiB,CAAC,aAA4B;IAC5D,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACP,GAAG,WAAW,CAAC,aAAa,CAAC;YAC7B,8BAA8B,EAAE,eAAe;YAC/C,8BAA8B,EAAE,eAAe;YAC/C,wBAAwB,EAAE,OAAO;SAClC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,QAAkB,EAClB,aAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;QACvE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;QACjC,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,OAAO;KACR,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * remoteStorage discovery (draft §10): the WebFinger link advertising a user's
3
+ * storage root and the OAuth endpoint a connecting app uses.
4
+ *
5
+ * A no-backend app is given only a user address (`user@host`); it discovers the
6
+ * storage root by querying WebFinger for that account and reading the
7
+ * remoteStorage link. This module builds that link as a plain
8
+ * [`@dwk/webfinger`](../webfinger) `Link` so a deployment can drop it into its
9
+ * existing WebFinger resource record rather than standing up a second endpoint.
10
+ * Pure data — no Workers runtime.
11
+ */
12
+ import type { Link } from "@dwk/webfinger";
13
+ /** The remoteStorage WebFinger link relation (draft §10). */
14
+ export declare const REMOTESTORAGE_REL = "http://tools.ietf.org/id/draft-dejong-remotestorage";
15
+ /** The remoteStorage draft version advertised in the link properties. */
16
+ export declare const REMOTESTORAGE_VERSION = "draft-dejong-remotestorage-22";
17
+ /** Inputs for {@link remoteStorageLink}. */
18
+ export interface RemoteStorageLinkConfig {
19
+ /** The user's storage root URL (the `href` apps GET/PUT/DELETE under). */
20
+ readonly storageRoot: string;
21
+ /** The OAuth authorization endpoint (RFC 6749 §4.2 implicit grant). */
22
+ readonly authEndpoint: string;
23
+ }
24
+ /**
25
+ * Build the WebFinger remoteStorage `Link` for one account. The `properties`
26
+ * carry the spec version, the OAuth authorization URL, and the capability flags
27
+ * remoteStorage clients read (`Bearer` auth, `Content-Range` support); a `null`
28
+ * property value means "advertised as absent", per RFC 7033.
29
+ */
30
+ export declare function remoteStorageLink(config: RemoteStorageLinkConfig): Link;
31
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAE3C,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,wDACyB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAErE,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAYvE"}