@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.
- package/CHANGELOG.md +156 -0
- package/dist/providers/collaboration_provider.d.ts.map +1 -1
- package/dist/providers/collaboration_provider.js +4 -0
- package/dist/providers/collaboration_provider.js.map +1 -1
- package/dist/src/auth/secret.d.ts +48 -0
- package/dist/src/auth/secret.d.ts.map +1 -0
- package/dist/src/auth/secret.js +95 -0
- package/dist/src/auth/secret.js.map +1 -0
- package/dist/src/auth/token.d.ts +111 -14
- package/dist/src/auth/token.d.ts.map +1 -1
- package/dist/src/auth/token.js +95 -14
- package/dist/src/auth/token.js.map +1 -1
- package/dist/src/collaboration_manager.d.ts +72 -7
- package/dist/src/collaboration_manager.d.ts.map +1 -1
- package/dist/src/collaboration_manager.js +131 -1
- package/dist/src/collaboration_manager.js.map +1 -1
- package/dist/src/define_config.d.ts +5 -0
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/documents.d.ts +100 -0
- package/dist/src/documents.d.ts.map +1 -1
- package/dist/src/documents.js +49 -0
- package/dist/src/documents.js.map +1 -1
- package/dist/src/driver.d.ts +53 -0
- package/dist/src/driver.d.ts.map +1 -1
- package/dist/src/driver.js +4 -1
- package/dist/src/driver.js.map +1 -1
- package/dist/src/drivers/automerge/automerge_driver.d.ts +25 -0
- package/dist/src/drivers/automerge/automerge_driver.d.ts.map +1 -1
- package/dist/src/drivers/automerge/automerge_driver.js +105 -16
- package/dist/src/drivers/automerge/automerge_driver.js.map +1 -1
- package/dist/src/drivers/partykit/partykit_driver.d.ts +5 -0
- package/dist/src/drivers/partykit/partykit_driver.d.ts.map +1 -1
- package/dist/src/drivers/partykit/partykit_driver.js +13 -7
- package/dist/src/drivers/partykit/partykit_driver.js.map +1 -1
- package/dist/src/drivers/yjs/yjs_driver.d.ts +16 -4
- package/dist/src/drivers/yjs/yjs_driver.d.ts.map +1 -1
- package/dist/src/drivers/yjs/yjs_driver.js +168 -17
- package/dist/src/drivers/yjs/yjs_driver.js.map +1 -1
- package/dist/src/http/types.d.ts +21 -3
- package/dist/src/http/types.d.ts.map +1 -1
- package/dist/src/http/types.js.map +1 -1
- package/dist/src/index.d.ts +9 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/observability.d.ts +11 -0
- package/dist/src/observability.d.ts.map +1 -1
- package/dist/src/observability.js +18 -0
- package/dist/src/observability.js.map +1 -1
- package/dist/src/routes.d.ts +74 -2
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +147 -9
- package/dist/src/routes.js.map +1 -1
- package/dist/src/types.d.ts +45 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/stubs/config/collaboration.stub +8 -0
- package/dist/stubs/partykit/worker/party.ts.stub +35 -4
- 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,
|
|
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":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAEvE,OAAO,EAAqB,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,OAAO,4BAA4B;IAGzB,GAAG;IAFjB,MAAM,CAA0B;IAExC,YAAsB,GAAuB;mBAAvB,GAAG;IAAuB,CAAC;IAEjD,QAAQ;QACN,+FAA+F;QAC/F,0FAA0F;QAC1F,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEvB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAyB,eAAe,CAAC,CAAC;QAC5E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,oBAAoB,EAAE,GAAG,EAAE;YACtD,MAAM,aAAa,GACjB,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;gBAC3C,CAAC,CAAC,IAAI,kBAAkB,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACpD,CAAC,CAAC,SAAS,CAAC;YAEhB,MAAM,aAAa,GAAG;gBACpB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,KAAK;gBAC9B,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5D,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5D,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5D,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,gBAAgB;gBACrC,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;gBACjC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACtD,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/D,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1D,CAAC;YAEF,OAAO,IAAI,oBAAoB,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI;QACR,uEAAuE;QACvE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAA4B,CAAC;YACpF,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACjD,sBAAsB,CAAC,MAAM,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;YACnE,8DAA8D;QAChE,CAAC;QAED,oEAAoE;QACpE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;QACnC,IAAI,MAAM,EAAE,OAAO,KAAK,KAAK,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAEnD,CAAC;QAEL,MAAM,cAAc,GAA8C;YAChE,qEAAqE;YACrE,uEAAuE;YACvE,eAAe,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC;YACpE,yEAAyE;YACzE,mEAAmE;YACnE,oEAAoE;YACpE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;gBAChC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;gBACpE,OAAO,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACzC,CAAC;SACF,CAAC;QACF,IAAI,MAAM,EAAE,MAAM,KAAK,SAAS;YAAE,cAAc,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QACxE,IAAI,MAAM,EAAE,UAAU,KAAK,SAAS;YAAE,cAAc,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACpF,IAAI,MAAM,EAAE,WAAW,KAAK,SAAS;YAAE,cAAc,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QAEvF,mBAAmB,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,KAAK;QACT,uEAAuE;QACvE,0EAA0E;QAC1E,mEAAmE;QACnE,sEAAsE;QACtE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAY,CAAC;QAChD,IAAI,GAAG,KAAK,KAAK,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAEpE,MAAM,YAAY,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAE5D,CAAC;QAEF,MAAM,UAAU,GAAG,YAAY,CAAC,aAAa,EAAE,EAAE,CAAC;QAClD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,4GAA4G,CAC7G,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,sBAAsB,CAAC,SAAS,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QACpE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF"}
|
|
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"}
|
package/dist/src/auth/token.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
/**
|
|
23
|
-
|
|
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
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
export declare function
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
/**
|
|
38
|
-
export declare function
|
|
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;
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../src/auth/token.ts"],"names":[],"mappings":"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"}
|