@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/dist/jwt.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Minimal JWT decode and JWKS-based signature verification for the built-in
3
+ * bearer-token verifier.
4
+ *
5
+ * remoteStorage uses **plain OAuth 2.0 bearer tokens** (no DPoP); a deployment
6
+ * that issues self-contained JWT access tokens can have this package verify them
7
+ * directly against the issuer's public JWKS. The module decodes the compact JWS,
8
+ * selects the key by `kid`/`kty`, and verifies with Web Crypto; claim policy
9
+ * (issuer/audience/expiry/`scope`) lives in `auth.ts`.
10
+ *
11
+ * Only asymmetric algorithms are accepted. `HS*` and `none` are refused: an
12
+ * access token must be signed by the issuer's private key, never a symmetric
13
+ * secret a resource server could not safely hold.
14
+ */
15
+ import { base64urlToBytes, base64urlToText } from "./encoding";
16
+ // We avoid the DOM lib's algorithm-parameter type names (not declared by
17
+ // @cloudflare/workers-types); these objects are accepted structurally by
18
+ // crypto.subtle.importKey / verify, mirroring `@dwk/dpop` and `@dwk/solid-pod`.
19
+ const ALGS = {
20
+ ES256: {
21
+ kty: "EC",
22
+ crv: "P-256",
23
+ importParams: { name: "ECDSA", namedCurve: "P-256" },
24
+ verifyParams: { name: "ECDSA", hash: "SHA-256" },
25
+ },
26
+ ES384: {
27
+ kty: "EC",
28
+ crv: "P-384",
29
+ importParams: { name: "ECDSA", namedCurve: "P-384" },
30
+ verifyParams: { name: "ECDSA", hash: "SHA-384" },
31
+ },
32
+ RS256: {
33
+ kty: "RSA",
34
+ importParams: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
35
+ verifyParams: { name: "RSASSA-PKCS1-v1_5" },
36
+ },
37
+ PS256: {
38
+ kty: "RSA",
39
+ importParams: { name: "RSA-PSS", hash: "SHA-256" },
40
+ verifyParams: { name: "RSA-PSS", saltLength: 32 },
41
+ },
42
+ };
43
+ function isObject(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ /**
47
+ * Decode a compact JWS into its header, payload, and signature. Returns `null`
48
+ * for anything that is not three base64url segments with JSON header/payload.
49
+ */
50
+ export function decodeJwt(token) {
51
+ if (typeof token !== "string")
52
+ return null;
53
+ const segments = token.split(".");
54
+ if (segments.length !== 3)
55
+ return null;
56
+ const [headerSeg, payloadSeg, signatureSeg] = segments;
57
+ try {
58
+ const header = JSON.parse(base64urlToText(headerSeg));
59
+ const payload = JSON.parse(base64urlToText(payloadSeg));
60
+ if (!isObject(header) || !isObject(payload))
61
+ return null;
62
+ return {
63
+ header,
64
+ payload,
65
+ signingInput: `${headerSeg}.${payloadSeg}`,
66
+ signature: base64urlToBytes(signatureSeg),
67
+ };
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ /** Whether a JWK could verify a signature under the given algorithm. */
74
+ function keyMatchesAlg(jwk, spec) {
75
+ if (jwk.kty !== spec.kty)
76
+ return false;
77
+ if (spec.crv !== undefined && jwk.crv !== spec.crv)
78
+ return false;
79
+ return true;
80
+ }
81
+ /**
82
+ * Verify `decoded`'s signature against a JWKS. Selects the verification key by
83
+ * `kid` when the header carries one, otherwise tries every key compatible with
84
+ * the header `alg`. Returns `true` on the first key that verifies.
85
+ */
86
+ export async function verifyJwtSignature(decoded, jwks) {
87
+ const alg = decoded.header.alg;
88
+ const spec = typeof alg === "string" ? ALGS[alg] : undefined;
89
+ if (!spec)
90
+ return false;
91
+ const kid = decoded.header.kid;
92
+ const compatible = jwks.filter((jwk) => keyMatchesAlg(jwk, spec));
93
+ // When the token names a `kid`, pin verification to the key(s) carrying it; a
94
+ // `kid` matching no JWKS key is rejected rather than verified against
95
+ // unrelated keys (which would silently weaken `kid` pinning). Only when the
96
+ // header omits `kid` do we try every alg-compatible key.
97
+ let candidates = compatible;
98
+ if (typeof kid === "string") {
99
+ candidates = compatible.filter((jwk) => jwk.kid === kid);
100
+ if (candidates.length === 0)
101
+ return false;
102
+ }
103
+ // The signing input is the same for every candidate key; encode it once.
104
+ const signingInput = new TextEncoder().encode(decoded.signingInput);
105
+ for (const jwk of candidates) {
106
+ try {
107
+ const key = await crypto.subtle.importKey("jwk", jwk, spec.importParams, false, ["verify"]);
108
+ const ok = await crypto.subtle.verify(spec.verifyParams, key, decoded.signature, signingInput);
109
+ if (ok)
110
+ return true;
111
+ }
112
+ catch {
113
+ // A key that fails to import (wrong shape) simply does not match; try the
114
+ // next candidate rather than aborting the whole verification.
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+ //# sourceMappingURL=jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@dwk/remotestorage` — structured observability event taxonomy.
3
+ *
4
+ * The vault authenticates OAuth bearer tokens and enforces per-module scopes at
5
+ * the edge; an auth/authz decision that is silently swallowed is an operational
6
+ * blind spot. Logging and metrics are opt-in via an injected {@link Logger} and
7
+ * {@link Metrics} (see `@dwk/log`) wired **once at the composition boundary**
8
+ * (the stateless front door). They **share this one vocabulary**: the same
9
+ * dotted event name is passed to the logger and the metrics sink so a log line
10
+ * and its counter line up.
11
+ *
12
+ * Fields follow the redaction policy: only machine-readable reason codes, HTTP
13
+ * method/status, and a sanitized subject host — never tokens, scopes verbatim,
14
+ * resource paths, or bodies. See `spec/observability.md`.
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ /** Stable event names emitted by `@dwk/remotestorage`. */
19
+ export declare const RemoteStorageLogEvent: {
20
+ /** A presented bearer token failed verification. Field: `reason`. */
21
+ readonly AuthRejected: "remotestorage.auth.rejected";
22
+ /** A request authenticated successfully at the edge. Field: `subjectHost`. */
23
+ readonly AuthAccepted: "remotestorage.auth.accepted";
24
+ /** A request lacked the scope (or auth) for the target. Fields: `method`, `status` (401/403). */
25
+ readonly AccessDenied: "remotestorage.scope.denied";
26
+ };
27
+ /** Union of the event-name string literals in {@link RemoteStorageLogEvent}. */
28
+ export type RemoteStorageLogEvent = (typeof RemoteStorageLogEvent)[keyof typeof RemoteStorageLogEvent];
29
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,qBAAqB;IAChC,qEAAqE;;IAErE,8EAA8E;;IAE9E,iGAAiG;;CAEzF,CAAC;AAEX,gFAAgF;AAChF,MAAM,MAAM,qBAAqB,GAC/B,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,OAAO,qBAAqB,CAAC,CAAC"}
package/dist/log.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@dwk/remotestorage` — structured observability event taxonomy.
3
+ *
4
+ * The vault authenticates OAuth bearer tokens and enforces per-module scopes at
5
+ * the edge; an auth/authz decision that is silently swallowed is an operational
6
+ * blind spot. Logging and metrics are opt-in via an injected {@link Logger} and
7
+ * {@link Metrics} (see `@dwk/log`) wired **once at the composition boundary**
8
+ * (the stateless front door). They **share this one vocabulary**: the same
9
+ * dotted event name is passed to the logger and the metrics sink so a log line
10
+ * and its counter line up.
11
+ *
12
+ * Fields follow the redaction policy: only machine-readable reason codes, HTTP
13
+ * method/status, and a sanitized subject host — never tokens, scopes verbatim,
14
+ * resource paths, or bodies. See `spec/observability.md`.
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ /** Stable event names emitted by `@dwk/remotestorage`. */
19
+ export const RemoteStorageLogEvent = {
20
+ /** A presented bearer token failed verification. Field: `reason`. */
21
+ AuthRejected: "remotestorage.auth.rejected",
22
+ /** A request authenticated successfully at the edge. Field: `subjectHost`. */
23
+ AuthAccepted: "remotestorage.auth.accepted",
24
+ /** A request lacked the scope (or auth) for the target. Fields: `method`, `status` (401/403). */
25
+ AccessDenied: "remotestorage.scope.denied",
26
+ };
27
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,qEAAqE;IACrE,YAAY,EAAE,6BAA6B;IAC3C,8EAA8E;IAC9E,YAAY,EAAE,6BAA6B;IAC3C,iGAAiG;IACjG,YAAY,EAAE,4BAA4B;CAClC,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The remoteStorage authorization model as pure, runtime-free logic: OAuth
3
+ * **scope** parsing, the module a storage path belongs to, and the read/write
4
+ * access decision. No Workers runtime, no store, no token verification — so it
5
+ * unit-tests in isolation and the front door and any future co-tenant share one
6
+ * audited rule set.
7
+ *
8
+ * remoteStorage scopes are space-delimited `<module>:<mode>` entries (draft
9
+ * §10): `mode` is `r` (read-only) or `rw` (read-write), and the module is a
10
+ * top-level folder name, or `*` for the whole vault. A module scope `m` grants
11
+ * access to both the private tree `/<m>/…` and the public tree `/public/<m>/…`.
12
+ */
13
+ /** A granted access mode: read-only or read-write. */
14
+ export type ScopeMode = "r" | "rw";
15
+ /** A single parsed remoteStorage OAuth scope. */
16
+ export interface RemoteStorageScope {
17
+ /** Top-level module (folder) name, or {@link ROOT_MODULE} for the whole vault. */
18
+ readonly module: string;
19
+ /** Whether the scope grants writes (`rw`) or reads only (`r`). */
20
+ readonly mode: ScopeMode;
21
+ }
22
+ /** The wildcard module name granting access to the entire vault. */
23
+ export declare const ROOT_MODULE = "*";
24
+ /**
25
+ * Parse a space-delimited OAuth `scope` string into remoteStorage scopes.
26
+ * Malformed entries (no `:`, empty module, or a mode other than `r`/`rw`) are
27
+ * dropped rather than failing the whole token, so an unrelated scope a client
28
+ * also requested cannot deny access to a valid one.
29
+ */
30
+ export declare function parseScopes(raw: string | undefined | null): RemoteStorageScope[];
31
+ /** Whether a request path targets a folder (the trailing-slash convention). */
32
+ export declare function isFolderPath(path: string): boolean;
33
+ /**
34
+ * Whether a path is a **publicly readable document**: it lives under `/public/`
35
+ * and is not itself a folder. Folder listings are never public, even under
36
+ * `/public/` (draft §10), so a trailing-slash path returns `false`.
37
+ */
38
+ export declare function isPublicDocument(path: string): boolean;
39
+ /**
40
+ * The top-level module a storage path belongs to, for scope matching. The
41
+ * leading `/public/` segment is transparent — `/public/photos/x` and
42
+ * `/photos/x` share the module `photos` — and the vault root (`/`, or
43
+ * `/public/`) maps to the empty module, which only the {@link ROOT_MODULE}
44
+ * (`*`) scope can reach.
45
+ */
46
+ export declare function moduleForPath(path: string): string;
47
+ /**
48
+ * Decide whether the granted `scopes` permit the requested access to `path`.
49
+ * `needWrite` selects read-vs-write: a write requires an `rw` scope, a read is
50
+ * satisfied by either. A scope matches when its module equals the path's module
51
+ * or is the `*` wildcard. The empty-module root (`/`) is reachable only via `*`.
52
+ */
53
+ export declare function authorizeScopes(scopes: readonly RemoteStorageScope[], path: string, needWrite: boolean): boolean;
54
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,sDAAsD;AACtD,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC;AAEnC,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;CAC1B;AAED,oEAAoE;AACpE,eAAO,MAAM,WAAW,MAAM,CAAC;AAE/B;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAC7B,kBAAkB,EAAE,CAatB;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,OAAO,GACjB,OAAO,CAQT"}
package/dist/scope.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The remoteStorage authorization model as pure, runtime-free logic: OAuth
3
+ * **scope** parsing, the module a storage path belongs to, and the read/write
4
+ * access decision. No Workers runtime, no store, no token verification — so it
5
+ * unit-tests in isolation and the front door and any future co-tenant share one
6
+ * audited rule set.
7
+ *
8
+ * remoteStorage scopes are space-delimited `<module>:<mode>` entries (draft
9
+ * §10): `mode` is `r` (read-only) or `rw` (read-write), and the module is a
10
+ * top-level folder name, or `*` for the whole vault. A module scope `m` grants
11
+ * access to both the private tree `/<m>/…` and the public tree `/public/<m>/…`.
12
+ */
13
+ /** The wildcard module name granting access to the entire vault. */
14
+ export const ROOT_MODULE = "*";
15
+ /**
16
+ * Parse a space-delimited OAuth `scope` string into remoteStorage scopes.
17
+ * Malformed entries (no `:`, empty module, or a mode other than `r`/`rw`) are
18
+ * dropped rather than failing the whole token, so an unrelated scope a client
19
+ * also requested cannot deny access to a valid one.
20
+ */
21
+ export function parseScopes(raw) {
22
+ if (!raw)
23
+ return [];
24
+ const scopes = [];
25
+ for (const entry of raw.split(/\s+/)) {
26
+ if (entry === "")
27
+ continue;
28
+ const colon = entry.lastIndexOf(":");
29
+ if (colon <= 0)
30
+ continue;
31
+ const module = entry.slice(0, colon);
32
+ const mode = entry.slice(colon + 1);
33
+ if (mode !== "r" && mode !== "rw")
34
+ continue;
35
+ scopes.push({ module, mode });
36
+ }
37
+ return scopes;
38
+ }
39
+ /** Whether a request path targets a folder (the trailing-slash convention). */
40
+ export function isFolderPath(path) {
41
+ return path.endsWith("/");
42
+ }
43
+ /**
44
+ * Whether a path is a **publicly readable document**: it lives under `/public/`
45
+ * and is not itself a folder. Folder listings are never public, even under
46
+ * `/public/` (draft §10), so a trailing-slash path returns `false`.
47
+ */
48
+ export function isPublicDocument(path) {
49
+ return path.startsWith("/public/") && !isFolderPath(path);
50
+ }
51
+ /**
52
+ * The top-level module a storage path belongs to, for scope matching. The
53
+ * leading `/public/` segment is transparent — `/public/photos/x` and
54
+ * `/photos/x` share the module `photos` — and the vault root (`/`, or
55
+ * `/public/`) maps to the empty module, which only the {@link ROOT_MODULE}
56
+ * (`*`) scope can reach.
57
+ */
58
+ export function moduleForPath(path) {
59
+ let rel = path.replace(/^\/+/, "");
60
+ if (rel === "public" || rel.startsWith("public/")) {
61
+ rel = rel.slice("public".length).replace(/^\/+/, "");
62
+ }
63
+ const slash = rel.indexOf("/");
64
+ return slash === -1 ? rel : rel.slice(0, slash);
65
+ }
66
+ /**
67
+ * Decide whether the granted `scopes` permit the requested access to `path`.
68
+ * `needWrite` selects read-vs-write: a write requires an `rw` scope, a read is
69
+ * satisfied by either. A scope matches when its module equals the path's module
70
+ * or is the `*` wildcard. The empty-module root (`/`) is reachable only via `*`.
71
+ */
72
+ export function authorizeScopes(scopes, path, needWrite) {
73
+ const module = moduleForPath(path);
74
+ for (const scope of scopes) {
75
+ if (scope.module !== ROOT_MODULE && scope.module !== module)
76
+ continue;
77
+ if (needWrite && scope.mode !== "rw")
78
+ continue;
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,GAA8B;IAE9B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,MAAM,MAAM,GAAyB,EAAE,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,KAAK,KAAK,EAAE;YAAE,SAAS;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,KAAK,IAAI,CAAC;YAAE,SAAS;QACzB,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QACpC,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI;YAAE,SAAS;QAC5C,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnC,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAClD,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAqC,EACrC,IAAY,EACZ,SAAkB;IAElB,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM;YAAE,SAAS;QACtE,IAAI,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;YAAE,SAAS;QAC/C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The per-account Durable Object: the single-threaded consistency authority for
3
+ * one remoteStorage vault.
4
+ *
5
+ * The stateless front door (`handler.ts`) authenticates the bearer token and
6
+ * enforces per-module scopes at the edge, then forwards already-authorized
7
+ * requests here, where Cloudflare guarantees one thread per account. Everything
8
+ * that must be strongly consistent — document GET/PUT/DELETE, conditional
9
+ * (`If-Match`/`If-None-Match`) writes, document↔folder conflict detection, the
10
+ * virtual folder listing and its aggregate ETag, and R2 copy-on-write through
11
+ * `@dwk/store` — happens here. This object reuses `@dwk/store` exactly as
12
+ * `@dwk/solid-pod` does (same library, same storage primitives); the only Solid
13
+ * facility it leans on beyond the blob tier is the generic `list(prefix)`
14
+ * projection. Consumers bind this class as a Durable Object namespace.
15
+ */
16
+ import { DurableObject } from "cloudflare:workers";
17
+ import { type RemoteStorageEnv } from "./config";
18
+ export declare class RemoteStorageObject extends DurableObject<RemoteStorageEnv> {
19
+ #private;
20
+ fetch(request: Request): Promise<Response>;
21
+ }
22
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAWnD,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAyBnE,qBAAa,mBAAoB,SAAQ,aAAa,CAAC,gBAAgB,CAAC;;IAevD,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;CAyO1D"}