@adonis-agora/collaboration 0.11.0 → 0.13.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 (59) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/providers/collaboration_provider.d.ts.map +1 -1
  3. package/dist/providers/collaboration_provider.js +4 -0
  4. package/dist/providers/collaboration_provider.js.map +1 -1
  5. package/dist/src/auth/secret.d.ts +48 -0
  6. package/dist/src/auth/secret.d.ts.map +1 -0
  7. package/dist/src/auth/secret.js +95 -0
  8. package/dist/src/auth/secret.js.map +1 -0
  9. package/dist/src/auth/token.d.ts +111 -14
  10. package/dist/src/auth/token.d.ts.map +1 -1
  11. package/dist/src/auth/token.js +95 -14
  12. package/dist/src/auth/token.js.map +1 -1
  13. package/dist/src/collaboration_manager.d.ts +72 -7
  14. package/dist/src/collaboration_manager.d.ts.map +1 -1
  15. package/dist/src/collaboration_manager.js +131 -1
  16. package/dist/src/collaboration_manager.js.map +1 -1
  17. package/dist/src/define_config.d.ts +5 -0
  18. package/dist/src/define_config.d.ts.map +1 -1
  19. package/dist/src/define_config.js.map +1 -1
  20. package/dist/src/documents.d.ts +100 -0
  21. package/dist/src/documents.d.ts.map +1 -1
  22. package/dist/src/documents.js +49 -0
  23. package/dist/src/documents.js.map +1 -1
  24. package/dist/src/driver.d.ts +53 -0
  25. package/dist/src/driver.d.ts.map +1 -1
  26. package/dist/src/driver.js +4 -1
  27. package/dist/src/driver.js.map +1 -1
  28. package/dist/src/drivers/automerge/automerge_driver.d.ts +25 -0
  29. package/dist/src/drivers/automerge/automerge_driver.d.ts.map +1 -1
  30. package/dist/src/drivers/automerge/automerge_driver.js +105 -16
  31. package/dist/src/drivers/automerge/automerge_driver.js.map +1 -1
  32. package/dist/src/drivers/partykit/partykit_driver.d.ts +5 -0
  33. package/dist/src/drivers/partykit/partykit_driver.d.ts.map +1 -1
  34. package/dist/src/drivers/partykit/partykit_driver.js +13 -7
  35. package/dist/src/drivers/partykit/partykit_driver.js.map +1 -1
  36. package/dist/src/drivers/yjs/yjs_driver.d.ts +16 -4
  37. package/dist/src/drivers/yjs/yjs_driver.d.ts.map +1 -1
  38. package/dist/src/drivers/yjs/yjs_driver.js +168 -17
  39. package/dist/src/drivers/yjs/yjs_driver.js.map +1 -1
  40. package/dist/src/http/types.d.ts +21 -3
  41. package/dist/src/http/types.d.ts.map +1 -1
  42. package/dist/src/http/types.js.map +1 -1
  43. package/dist/src/index.d.ts +9 -6
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js +6 -4
  46. package/dist/src/index.js.map +1 -1
  47. package/dist/src/observability.d.ts +11 -0
  48. package/dist/src/observability.d.ts.map +1 -1
  49. package/dist/src/observability.js +18 -0
  50. package/dist/src/observability.js.map +1 -1
  51. package/dist/src/routes.d.ts +74 -2
  52. package/dist/src/routes.d.ts.map +1 -1
  53. package/dist/src/routes.js +147 -9
  54. package/dist/src/routes.js.map +1 -1
  55. package/dist/src/types.d.ts +45 -0
  56. package/dist/src/types.d.ts.map +1 -1
  57. package/dist/stubs/config/collaboration.stub +8 -0
  58. package/dist/stubs/partykit/worker/party.ts.stub +35 -4
  59. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,161 @@
1
1
  # @adonis-agora/collaboration
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 50016c7: A `load` hook per document, and a public API for the live document.
8
+
9
+ `documents` entries take a `load({ docName, params })` — the document's **first** content, asked
10
+ for only when the storage has never stored that name, and persisted before the document is served.
11
+ Return the engine's binary state (`Y.encodeStateAsUpdate`, `A.save`), a `Y.Doc` on the Yjs engine,
12
+ or `null` for "nothing to seed"; `params` is typed from the pattern key exactly like `authorize`'s.
13
+ It runs once per document rather than once per connection, a throw is reported and opens the
14
+ document empty instead of failing the handshake, and two connections racing on a new document seed
15
+ it once. Both self-hosted engines have it.
16
+
17
+ It exists because the alternative is not "the app seeds it": it is "the app seeds it from a request
18
+ it believes precedes the WebSocket". Losing that race is destructive rather than merely wrong — the
19
+ editor mounts against an empty CRDT document, that is what it shows, and the first autosave writes
20
+ the emptiness over the text in the database.
21
+
22
+ `CollaborationManager` gains `withLiveDocument({ docName, run })`, `hasLiveDocument({ docName })`
23
+ and `getVersionState({ docName, versionId })`. `withLiveDocument` lends the real in-process
24
+ document to a callback — an external change (a Drive webhook, a cron) has to merge into the
25
+ instance people are typing into, and `getDocumentState` only ever hands out a copy — and answers
26
+ `{ live: true, value }` or `{ live: false, reason }`, so a document nobody has open, or one on an
27
+ engine that syncs elsewhere, is an answer instead of an invalid cast. The app that needed this was
28
+ reaching `manager.drivers.get('yjs').liveDoc(name)` through a cast onto two private fields.
29
+
30
+ Fixes a silent bug in the Yjs driver on the way: `onLoadDocument` returned `{ document }`, the
31
+ Hocuspocus v1 shape, and Hocuspocus 4 applies the hook's result only when it is a `Y.Doc` or a
32
+ `Uint8Array` — so the persisted state was read, handed to a branch that ignored it, and every
33
+ socket got an empty document. Only a test calling the hook directly could still see it "work".
34
+
35
+ `YjsDriver.liveDoc` and `YjsDriver.versionState` were private; they are now `liveDocument` and
36
+ `getVersionState`, the latter on the `CollaborationDriver` contract (implemented by all three
37
+ drivers). Nothing on `CollaborationManager` changed shape — `getDocumentState`, `getDocumentText`,
38
+ `listVersions` and the rest keep working exactly as before.
39
+ - 50016c7: `issueCollaborationToken` and `initialTokens` — the token round-trip in front of the socket, removed.
40
+
41
+ Opening a document was two serialized requests: `GET /collaboration/token`, and only then the
42
+ WebSocket. Nothing of the text could arrive until both finished — and the first of them went to the
43
+ server that had just rendered the page and already knew the whole answer.
44
+
45
+ - **Server.** `issueCollaborationToken({ ctx, docName })` (or `{ user, docName }`) issues the
46
+ credential outside the route, for a controller to hand to the page it is rendering. It runs
47
+ `authorize`, resolves the document's engine through the manager and builds `wsUrl` through the
48
+ same function the endpoint calls — a token minted from a controller is byte-for-byte the one the
49
+ route would have issued for that caller. A shortcut that skipped the permission check because it
50
+ was reached from a controller would be a hole, not a shortcut. It throws (`CollabUnauthorizedError`,
51
+ `CollabForbiddenError`) rather than returning a response: in a page controller there is none to
52
+ return, and a render that fails is better than an editor shown to someone who may not read it.
53
+ - **Client.** `<CollaborationProvider initialTokens={{ [docName]: collabToken }}>` and
54
+ `useCollabDoc({ docName, token })`. The transport is built at mount with no HTTP request in front
55
+ of it.
56
+ - **Used once, deliberately.** Reconnects still fetch a fresh token: a credential minted at render
57
+ time is exactly the one that has expired by the time a socket drops, and re-fetching is what
58
+ re-runs `authorize` and makes revocation take effect. A pre-issued token that is malformed or
59
+ already dead is discarded and the session fetches one as it always did — a stale page prop can
60
+ cost the first paint, never the connection.
61
+ - `IssuedToken` (and the `/token` response) gained `expiresAt` for the formats that have an expiry,
62
+ so the client can recognise a dead page prop without opening a socket to find out. The self-hosted
63
+ base64 format still has none: `authorize` re-runs on the handshake, so it is a claim, not a grant
64
+ with a clock.
65
+
66
+ Nothing changes for an app that passes no token: same request, same order, same behaviour.
67
+ - 50016c7: **BREAKING — security.** The self-hosted token is a signed credential now, not an unsigned claim.
68
+
69
+ `issueToken` used to return `base64(JSON.stringify({ userId }))` for the `yjs` and `automerge`
70
+ engines, and the drivers accepted anything that decoded to an object with a `userId` in it. That is
71
+ not a credential: **a client with no session, no cookie and no login could connect with
72
+ `base64({"userId":"<someone else's id>"})`, be authorized as that person by the app's own
73
+ `authorize` rule, and write into their document.** Verified against the published library, not
74
+ theorised. The token also never expired and could not be revoked, and the WebSocket upgrade runs
75
+ outside the HTTP middleware stack, so no app-side middleware was standing in front of it.
76
+
77
+ If you run `yjs` or `automerge` with `@adonis-agora/collaboration` ≤ 0.12, treat this as an
78
+ urgent upgrade: any document reachable to an attacker who can guess a user id and a document name
79
+ was readable and writable by them.
80
+
81
+ **What changed**
82
+
83
+ - The self-hosted token is an HS256 JWT over `{ userId, docName, user?, iat, exp }`, the same wire
84
+ format the edge already used. The handshake checks the **signature**, the **expiry** (5 minutes,
85
+ `COLLAB_TOKEN_TTL_SECONDS`, matching the edge) and the **document** before `authorize` is
86
+ consulted at all — a token issued for one document does not open another, since the client is the
87
+ one choosing the name it connects to. Signature comparison is constant-time.
88
+ - Both self-hosted drivers verify: `YjsDriver.onAuthenticate` and the `AutomergeDriver` upgrade
89
+ handler (which now answers `4001` for a token that fails verification and `4503` when the server
90
+ has no key).
91
+ - Presence no longer re-decodes the raw token. The Yjs driver publishes the verified identity as the
92
+ connection context and fills the roster from it in `connected`; the old `onConnect` path decoded
93
+ `?token=` a second time, unauthenticated, so a forged name reached the roster even with a checked
94
+ handshake.
95
+ - `verifyPartyKitToken` takes the expected `docName` and enforces it, and the published worker
96
+ template compares the claim against the room it is connecting to. The edge had the same hole: the
97
+ signature was checked, the room was not.
98
+ - `IssuedToken.expiresAt` is now reported for self-hosted tokens too, so a pre-issued page prop can
99
+ be discarded by the client without opening a socket.
100
+
101
+ **What you have to do**
102
+
103
+ - Nothing, in a normal AdonisJS app: the signing key is derived from `appKey` (domain-separated), so
104
+ a project with `APP_KEY` set keeps working. Set `tokenSecret` in `config/collaboration.ts` for a
105
+ dedicated key.
106
+ - **If you construct `CollaborationManager` yourself**, pass `tokenSecret` — or run inside a booted
107
+ app so `appKey` is readable. With no key anywhere the library refuses to issue *and* refuses to
108
+ accept tokens (`CollabTokenSecretMissingError`, naming what to set). There is deliberately no flag
109
+ to accept the old format: it would reopen exactly this hole.
110
+ - **If you build a driver directly**, `SelfHostedDriverOptions.tokenSecret` is required.
111
+ - Tokens in the old format stop being accepted. Connected clients reconnect and fetch a new one, so
112
+ the visible cost of upgrading is one reconnect.
113
+ - `parseLegacyToken` is gone. `verifyCollabToken(token, secret, docName)` and
114
+ `verifySelfHostedToken(token, docName, secret?)` replace it; `signCollabToken`,
115
+ `resolveTokenSecret` and `COLLAB_TOKEN_TTL_SECONDS` are exported alongside them.
116
+ `PARTYKIT_TOKEN_TTL_SECONDS` still exists and is the same number.
117
+
118
+ ### Patch Changes
119
+
120
+ - 50016c7: `defaultResolveUser` answers 401 for a caller the app considers authenticated (authkit).
121
+
122
+ The resolver exists so that no app has to bolt a global middleware onto a hardcoded `/collaboration`
123
+ prefix: it calls `check()`/`authenticate()` itself and then reads `ctx.auth.user`. But
124
+ `@adonis-agora/authkit` resolves the user **on demand** — `check()` verifies the session and returns
125
+ `true`, and `ctx.auth.user` stays `undefined` forever, because the user only materialises when
126
+ something asks for it. So the routes 401'd, and every app hitting this wrote the same three-line
127
+ `resolveUser` calling `getUserOrFail()` by hand — the same call its own controllers make.
128
+
129
+ It now asks: after `check()`/`authenticate()`, when `auth.user` is still empty it tries `getUser()`
130
+ (absence reported by returning nothing) and then `getUserOrFail()` (absence reported by throwing).
131
+ Both steps are needed — one auth stack fills `user` as a side effect, another only answers a direct
132
+ question, and the point of this resolver is that no app has to know which one it installed.
133
+
134
+ Still fails closed, and still never 500s: a throwing `getUserOrFail()` is an absent user and a 401,
135
+ and an id that stringifies to nothing is a 401 too — an "empty" caller must never reach `authorize`,
136
+ where a blank id could match a rule.
137
+
138
+ A patch: no API changed and no app needs to do anything. A hand-written `resolveUser` keeps working
139
+ and can now be deleted.
140
+
141
+ ## 0.12.0
142
+
143
+ ### Minor Changes
144
+
145
+ - e911a53: `defineCollection(prefix, declaration?)` and a boot-time warning for patterns without a `:segment`.
146
+
147
+ A `documents` pattern only matches many documents when it has a `:segment` — a bare `'whiteboards'`
148
+ matches exactly one document named `whiteboards`, and `whiteboards/42` falls through to the globals.
149
+ That rule lived only inside the matcher, and getting it wrong was silent.
150
+
151
+ - `...defineCollection('whiteboards', { engine, authorize })` declares `'whiteboards/:id'` with
152
+ `params.id` typed, so the common shape cannot be written without the `:id`. The prefix is the
153
+ literal part of the name; a `:param` or a leading/trailing `/` throws.
154
+ - `CollaborationManager` warns once at boot for every declared pattern that has no `:segment`,
155
+ naming the pattern and the fix (`logger.warn` when the installed logger has one, else
156
+ `console.warn`). `CollabLogger` gains an optional `warn`.
157
+ - `documentPatternParams(pattern)` and `reportCollaborationWarning` are exported.
158
+
3
159
  ## 0.11.0
4
160
 
5
161
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"collaboration_provider.d.ts","sourceRoot":"","sources":["../../providers/collaboration_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAS/D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,OAAO,4BAA4B;IAGnC,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAF7C,OAAO,CAAC,MAAM,CAAC,CAAyB;IAExC,YAAsB,GAAG,EAAE,kBAAkB,EAAI;IAEjD,QAAQ,SA4BP;IAEK,IAAI,kBAuCT;IAEK,KAAK,kBAwBV;IAEK,QAAQ,kBAIb;CACF"}
1
+ {"version":3,"file":"collaboration_provider.d.ts","sourceRoot":"","sources":["../../providers/collaboration_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAS/D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,OAAO,4BAA4B;IAGnC,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAF7C,OAAO,CAAC,MAAM,CAAC,CAAyB;IAExC,YAAsB,GAAG,EAAE,kBAAkB,EAAI;IAEjD,QAAQ,SAgCP;IAEK,IAAI,kBAuCT;IAEK,KAAK,kBAwBV;IAEK,QAAQ,kBAIb;CACF"}
@@ -49,6 +49,10 @@ export default class CollaborationServiceProvider {
49
49
  path: config.path ?? '/collaboration',
50
50
  debounce: config.debounce ?? 2000,
51
51
  ...(config.storage ? { storage: config.storage } : {}),
52
+ // Absent means "derive from appKey" — the manager resolves it, and it
53
+ // must be the manager doing so, because the token route asks it for
54
+ // the same key.
55
+ ...(config.tokenSecret ? { tokenSecret: config.tokenSecret } : {}),
52
56
  ...(config.redisUrl ? { redis: { url: config.redisUrl } } : {}),
53
57
  ...(config.partykit ? { partykit: config.partykit } : {}),
54
58
  };
@@ -1 +1 @@
1
- {"version":3,"file":"collaboration_provider.js","sourceRoot":"","sources":["../../providers/collaboration_provider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collaboration_provider.js","sourceRoot":"","sources":["../../providers/collaboration_provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Where the signing key of a self-hosted collaboration token comes from.
3
+ *
4
+ * One resolver for both ends of the credential — the issuer (`issueToken`,
5
+ * behind `GET /collaboration/token` and `issueCollaborationToken`) and the
6
+ * verifier (the drivers' handshake). They MUST read the same value: a token
7
+ * signed with one key and checked against another is not a security failure,
8
+ * it is a total outage where every handshake is refused and nothing in the
9
+ * logs says why.
10
+ */
11
+ /**
12
+ * A secret, or a way to get one.
13
+ *
14
+ * The function form exists because the manager resolves its key lazily: the
15
+ * default source is the AdonisJS `appKey`, which only exists once the app has
16
+ * booted, and a driver is built before the first document is ever opened.
17
+ */
18
+ export type CollabTokenSecret = string | (() => string | Promise<string>);
19
+ /**
20
+ * Thrown when nothing anywhere declares a signing key.
21
+ *
22
+ * Fails closed, and says exactly what to set. The alternative — falling back
23
+ * to an unsigned token when the key is missing — is the hole this whole
24
+ * module exists to close: it would turn a misconfiguration into a server that
25
+ * accepts `base64({"userId":"<anyone>"})` from an unauthenticated client.
26
+ */
27
+ export declare class CollabTokenSecretMissingError extends Error {
28
+ constructor();
29
+ }
30
+ /**
31
+ * The key both ends of the credential use — the first source that has one:
32
+ *
33
+ * 1. what the caller passed (`CollaborationManager`'s `tokenSecret` option —
34
+ * the path a test or a non-Adonis embedder takes);
35
+ * 2. `tokenSecret` in `config/collaboration.ts`;
36
+ * 3. the app's `appKey`, domain-separated — so a normal AdonisJS app needs no
37
+ * configuration at all, which is the only way a security default actually
38
+ * gets adopted.
39
+ *
40
+ * Nothing found is an error, never a fallback. See
41
+ * {@link CollabTokenSecretMissingError}.
42
+ *
43
+ * Idempotent for any explicit secret: resolving an already-resolved key
44
+ * returns it unchanged, so the manager can memoize one key and let the issuer
45
+ * and the driver each resolve it again without the two drifting apart.
46
+ */
47
+ export declare function resolveTokenSecret(source?: CollabTokenSecret): Promise<string>;
48
+ //# sourceMappingURL=secret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../../../src/auth/secret.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAE1E;;;;;;;GAOG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;IACtD,cAQC;CACF;AAyDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CASpF"}
@@ -0,0 +1,95 @@
1
+ import { createHmac } from 'node:crypto';
2
+ /**
3
+ * Thrown when nothing anywhere declares a signing key.
4
+ *
5
+ * Fails closed, and says exactly what to set. The alternative — falling back
6
+ * to an unsigned token when the key is missing — is the hole this whole
7
+ * module exists to close: it would turn a misconfiguration into a server that
8
+ * accepts `base64({"userId":"<anyone>"})` from an unauthenticated client.
9
+ */
10
+ export class CollabTokenSecretMissingError extends Error {
11
+ constructor() {
12
+ super('[@adonis-agora/collaboration] no signing secret available — refusing to issue or accept collaboration tokens. ' +
13
+ 'In an AdonisJS app this is normally the app key and needs no configuration — check that APP_KEY is set ' +
14
+ 'and config/app.ts exports `appKey`. Outside an app (or to use a dedicated key), set `tokenSecret` in ' +
15
+ 'config/collaboration.ts, or pass `tokenSecret` to the CollaborationManager.');
16
+ this.name = 'CollabTokenSecretMissingError';
17
+ }
18
+ }
19
+ /**
20
+ * Domain separation for the key derived from `appKey`.
21
+ *
22
+ * `appKey` already signs cookies and encrypts session payloads. Signing
23
+ * collaboration tokens with the very same bytes would let a value produced by
24
+ * one of those subsystems be replayed at another; one HMAC over a fixed label
25
+ * makes the key used here unusable anywhere else. `v1` is there so a future
26
+ * format change can invalidate every outstanding token by changing one string.
27
+ *
28
+ * Applied ONLY to the `appKey` fallback, never to a secret someone configured
29
+ * on purpose — which is what makes {@link resolveTokenSecret} idempotent, and
30
+ * therefore safe to run over a key it already resolved. The manager memoizes
31
+ * its key and hands it to both the issuer and the driver, and each of them
32
+ * resolves again; a derivation on every pass would give the two ends
33
+ * different keys and refuse every handshake.
34
+ */
35
+ const APP_KEY_DOMAIN = '@adonis-agora/collaboration:self-hosted-token:v1';
36
+ /** The app's `appKey`, domain-separated, when running inside a booted app. */
37
+ async function readAppKey() {
38
+ try {
39
+ // Dynamic + optional: unavailable in plain unit tests, and in any process
40
+ // that builds a manager without an AdonisJS app around it.
41
+ const app = (await import('@adonisjs/core/services/app')).default;
42
+ const key = app.config.get('app.appKey', undefined);
43
+ // AdonisJS 6 wraps it in a `Secret<string>` so it never lands in a log by
44
+ // accident; older apps (and tests) keep a plain string.
45
+ const raw = typeof key === 'string'
46
+ ? key
47
+ : typeof key?.release === 'function'
48
+ ? key.release()
49
+ : undefined;
50
+ if (typeof raw !== 'string' || raw.length === 0)
51
+ return undefined;
52
+ return createHmac('sha256', raw).update(APP_KEY_DOMAIN).digest('base64url');
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ /** `tokenSecret` declared in `config/collaboration.ts`, when there is one. */
59
+ async function readConfiguredSecret() {
60
+ try {
61
+ const app = (await import('@adonisjs/core/services/app')).default;
62
+ const secret = app.config.get('collaboration.tokenSecret', undefined);
63
+ return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /**
70
+ * The key both ends of the credential use — the first source that has one:
71
+ *
72
+ * 1. what the caller passed (`CollaborationManager`'s `tokenSecret` option —
73
+ * the path a test or a non-Adonis embedder takes);
74
+ * 2. `tokenSecret` in `config/collaboration.ts`;
75
+ * 3. the app's `appKey`, domain-separated — so a normal AdonisJS app needs no
76
+ * configuration at all, which is the only way a security default actually
77
+ * gets adopted.
78
+ *
79
+ * Nothing found is an error, never a fallback. See
80
+ * {@link CollabTokenSecretMissingError}.
81
+ *
82
+ * Idempotent for any explicit secret: resolving an already-resolved key
83
+ * returns it unchanged, so the manager can memoize one key and let the issuer
84
+ * and the driver each resolve it again without the two drifting apart.
85
+ */
86
+ export async function resolveTokenSecret(source) {
87
+ const explicit = typeof source === 'function' ? await source() : source;
88
+ const secret = (explicit && explicit.length > 0 ? explicit : undefined) ??
89
+ (await readConfiguredSecret()) ??
90
+ (await readAppKey());
91
+ if (!secret)
92
+ throw new CollabTokenSecretMissingError();
93
+ return secret;
94
+ }
95
+ //# sourceMappingURL=secret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.js","sourceRoot":"","sources":["../../../src/auth/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAsBzC;;;;;;;GAOG;AACH,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IACtD;QACE,KAAK,CACH,gHAAgH;YAC9G,yGAAyG;YACzG,uGAAuG;YACvG,6EAA6E,CAChF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAC9C,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,cAAc,GAAG,kDAAkD,CAAC;AAE1E,8EAA8E;AAC9E,KAAK,UAAU,UAAU;IACvB,IAAI,CAAC;QACH,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,6BAA6B,CAAC,CAAC,CAAC,OAEzD,CAAC;QACF,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;QACpD,0EAA0E;QAC1E,wDAAwD;QACxD,MAAM,GAAG,GACP,OAAO,GAAG,KAAK,QAAQ;YACrB,CAAC,CAAC,GAAG;YACL,CAAC,CAAC,OAAQ,GAA+C,EAAE,OAAO,KAAK,UAAU;gBAC/E,CAAC,CAAE,GAAkC,CAAC,OAAO,EAAE;gBAC/C,CAAC,CAAC,SAAS,CAAC;QAClB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAClE,OAAO,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,KAAK,UAAU,oBAAoB;IACjC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,6BAA6B,CAAC,CAAC,CAAC,OAEzD,CAAC;QACF,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,2BAA2B,EAAE,SAAS,CAAC,CAAC;QACtE,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAA0B;IACjE,MAAM,QAAQ,GAAG,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IACxE,MAAM,MAAM,GACV,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;QACxD,CAAC,MAAM,oBAAoB,EAAE,CAAC;QAC9B,CAAC,MAAM,UAAU,EAAE,CAAC,CAAC;IAEvB,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,6BAA6B,EAAE,CAAC;IACvD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,12 +1,21 @@
1
1
  import type { CollabConnectionContext, CollabPermission, PartyKitConfig } from '../types.js';
2
+ import { type CollabTokenSecret } from './secret.js';
2
3
  /**
3
4
  * The token/auth module — the single seam for issuing and verifying
4
5
  * collaboration credentials.
5
6
  *
6
- * Two wire formats, chosen by engine:
7
- * - `partykit` → HS256 JWT (verified by the edge worker via WebCrypto)
8
- * - self-hosted (`yjs`/`automerge`) → legacy base64 JSON payload accepted
9
- * by the driver handshake
7
+ * **One wire format, two secrets.** Both the edge (`partykit`) and the
8
+ * self-hosted engines (`yjs`/`automerge`) carry an HS256 JWT bound to one
9
+ * document and valid for {@link COLLAB_TOKEN_TTL_SECONDS}; they differ only
10
+ * in which key signs it (the worker's `jwtSecret` vs. the app's own key — see
11
+ * `auth/secret.ts`) and therefore in who can verify it.
12
+ *
13
+ * It used to differ in more than that: the self-hosted format was a bare
14
+ * base64 of the connection context, unsigned and without an expiry. That is
15
+ * not a credential — anyone could send `base64({"userId":"<victim>"})` with no
16
+ * session, no cookie and no login, and the driver would authorize the
17
+ * handshake as that user and let them write. The format is gone; a token that
18
+ * is not signed by this server is not accepted by it.
10
19
  *
11
20
  * `issueToken` also enforces `config.authorize` on the REST path — the
12
21
  * permission seam must hold outside the WebSocket handshake too.
@@ -16,26 +25,97 @@ export interface TokenIssuerConfig {
16
25
  /** WS path of the embedded Hocuspocus server (self-hosted engines). */
17
26
  path?: string;
18
27
  partykit?: Partial<Pick<PartyKitConfig, 'roomHost' | 'jwtSecret' | 'party'>>;
28
+ /**
29
+ * Signing key for the self-hosted format. Resolved through
30
+ * {@link resolveTokenSecret}, so the issuer and the driver that verifies
31
+ * the token read the same source and cannot drift apart.
32
+ */
33
+ tokenSecret?: CollabTokenSecret;
19
34
  }
20
35
  /** Minimal authorize contract — mirrors CollaborationConfig.authorize. */
21
36
  export type AuthorizeFn = (ctx: CollabConnectionContext, docName: string) => Promise<CollabPermission>;
22
- /** Signs a minimal HS256 JWT — no external dependency. */
23
- export declare function signPartyKitToken(payload: {
37
+ /**
38
+ * Lifetime of a collaboration token, in seconds.
39
+ *
40
+ * Named rather than inlined because three places need the same number: the
41
+ * signature's `exp`, the `expiresAt` the issuer reports back so a caller
42
+ * holding the token ahead of time can tell it is already dead, and the
43
+ * client's decision to discard a page-prop token instead of opening a socket
44
+ * with it.
45
+ *
46
+ * Five minutes is what the edge format has always used, and the self-hosted
47
+ * one now matches it deliberately: the token is spent within milliseconds of
48
+ * being issued (the browser fetches it and connects), and a long-lived one
49
+ * would only widen the window in which a leaked URL still opens a document.
50
+ * The socket it opens is not cut short by expiry — `authorize` runs once at
51
+ * the handshake and the connection then lives on its own.
52
+ */
53
+ export declare const COLLAB_TOKEN_TTL_SECONDS = 300;
54
+ /**
55
+ * The old name of {@link COLLAB_TOKEN_TTL_SECONDS}, kept because it is public
56
+ * API that apps and the worker template already read. One number, because one
57
+ * wire format: a second constant would eventually drift from this one and the
58
+ * drift would show up as tokens that expire earlier than the client thinks.
59
+ */
60
+ export declare const PARTYKIT_TOKEN_TTL_SECONDS = 300;
61
+ /** What a signed collaboration token carries. */
62
+ export interface CollabTokenPayload {
24
63
  userId: string;
64
+ /**
65
+ * The document the credential opens — and only that one. Verified against
66
+ * the document actually being connected to; without that check a token
67
+ * legitimately issued for a document the caller may read would also open
68
+ * every document they may not.
69
+ */
25
70
  docName: string;
71
+ /** Presence metadata, carried so the roster does not need a second lookup. */
26
72
  user?: {
27
73
  name?: string;
28
74
  avatarUrl?: string | null;
29
75
  };
30
- }, secret: string, expiresInSeconds?: number): string;
31
- /** Verifies signature + expiry of a token issued by {@link signPartyKitToken}. */
32
- export declare function verifyPartyKitToken(token: string, secret: string): {
33
- userId: string;
34
- docName: string;
76
+ }
77
+ /** Signs a minimal HS256 JWT — no external dependency. */
78
+ export declare function signCollabToken(payload: CollabTokenPayload, secret: string, expiresInSeconds?: number): string;
79
+ /**
80
+ * Verifies signature, expiry and document binding of a token issued by
81
+ * {@link signCollabToken}. Returns `null` for every failure — a verifier that
82
+ * distinguishes "bad signature" from "wrong document" in its return value
83
+ * invites a caller to treat one of them as recoverable.
84
+ *
85
+ * `expectedDocName` is required rather than optional on purpose: every caller
86
+ * knows which document is being opened, and an optional argument is one a
87
+ * caller forgets — which is exactly the bug this parameter exists to prevent.
88
+ */
89
+ export declare function verifyCollabToken(token: string, secret: string, expectedDocName: string): (CollabTokenPayload & {
35
90
  exp: number;
36
- } | null;
37
- /** Parses the legacy base64 payload used by self-hosted driver handshakes. */
38
- export declare function parseLegacyToken(token: string): CollabConnectionContext | null;
91
+ }) | null;
92
+ /** The connection context a verified token stands for. */
93
+ export declare function tokenContext(payload: CollabTokenPayload): CollabConnectionContext;
94
+ /**
95
+ * Verifies a self-hosted token against the key resolved from the app —
96
+ * what a driver's handshake calls.
97
+ *
98
+ * Throws (rather than returning `null`) when there is no key to verify
99
+ * against: a missing secret is a misconfiguration of the server, not a bad
100
+ * token from the client, and the two must not produce the same message in the
101
+ * logs. Either way nothing is authenticated.
102
+ */
103
+ export declare function verifySelfHostedToken(token: string, docName: string, source: CollabTokenSecret | undefined): Promise<CollabConnectionContext | null>;
104
+ /**
105
+ * Signs the edge JWT. Same format and same TTL as every other collaboration
106
+ * token; the difference is the key, which the PartyKit worker holds
107
+ * (`COLLAB_JWT_SECRET`) and this process only shares with it.
108
+ */
109
+ export declare const signPartyKitToken: typeof signCollabToken;
110
+ /**
111
+ * Verifies an edge JWT, including its document binding.
112
+ *
113
+ * The worker template runs the same three checks in WebCrypto — signature,
114
+ * `exp`, and `docName` against the room being connected to. Keep them in
115
+ * step: the room is the document, so a token that opens a room it was not
116
+ * issued for is the same hole on the edge that it is here.
117
+ */
118
+ export declare const verifyPartyKitToken: typeof verifyCollabToken;
39
119
  /** Builds the browser-facing WebSocket URL for an engine. */
40
120
  export declare function buildWsUrl(config: TokenIssuerConfig, docName: string): string;
41
121
  /** Result of a successful token issue. */
@@ -43,6 +123,23 @@ export interface IssuedToken {
43
123
  token: string;
44
124
  wsUrl: string;
45
125
  engine: string;
126
+ /**
127
+ * Epoch milliseconds after which the credential stops being accepted.
128
+ *
129
+ * Reported for every engine: both wire formats are signed JWTs with an
130
+ * `exp`, and the handshake refuses a token past it. (It was absent for the
131
+ * self-hosted format while that format had no expiry — an unsigned claim
132
+ * about identity that never died. It is a real credential now.)
133
+ *
134
+ * It exists for the benefit of a token handed to the browser *before* it
135
+ * connects (a server-rendered page prop). Such a token can sit in the HTML
136
+ * for as long as the tab is open, and the client has to be able to tell
137
+ * "already dead" from "worth trying" without opening a socket to find out.
138
+ *
139
+ * Optional on the type because a custom issuer may not have one to report;
140
+ * nothing this package issues omits it.
141
+ */
142
+ expiresAt?: number;
46
143
  }
47
144
  /** Thrown when authorize() denies read access for the document. */
48
145
  export declare class CollabForbiddenError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../src/auth/token.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7F;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAC9E;AAED,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,uBAAuB,EAC5B,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE/B,0DAA0D;AAC1D,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,EACjG,MAAM,EAAE,MAAM,EACd,gBAAgB,SAAM,GACrB,MAAM,CAQR;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoBzD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAO9E;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAS7E;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mEAAmE;AACnE,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAG5C;CACF;AAED;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAE/C,QAAQ,CAAC,OAAO,EAAE,MAAM;aACN,KAAK,CAAC,EAAE,OAAO;IAFnC,YACW,OAAO,EAAE,MAAM,EACN,KAAK,CAAC,EAAE,OAAO,EAIlC;CACF;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC1C,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,WAAW,GACtB,OAAO,CAAC,WAAW,CAAC,CA+BtB"}
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../src/auth/token.ts"],"names":[],"mappings":"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"}