webdaemon 11.4.1 → 11.4.3

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/src/js/Token.js DELETED
@@ -1,529 +0,0 @@
1
- /**
2
- * Encapsulates the functionality associated with a bearer token normally
3
- * passed in a request `x-tabserver-token` header as a base64-encoded
4
- * JSON object.
5
- *
6
- * Note the type definition annotations which are helpful when using this
7
- * Javascript class in Typescript.
8
- *
9
- * Example token:
10
- *
11
- * {
12
- * "iss": "http://foo.daemon/some/public.jwk",
13
- * "aud": "https://bar.daemon",
14
- * "sub": "https://foo.daemon",
15
- * "src": "https://some.com",
16
- * "scope": {
17
- * "http://localhost:9000/helloWorld1.html": "read write",
18
- * "party:control": "grant revoke"
19
- * }
20
- * "iat": 1698576567000,
21
- * "exp": 1698576344000,
22
- * "sig": "long base64 string"
23
- * }
24
- *
25
- * Generally:
26
- * iss - the issuer URL references the public JWK used to verify the sig.
27
- * aud - the party handling the request, expressed as an origin.
28
- * sub - the counterparty making the request, expressed as an origin.
29
- * src - the source HTML file making the request, protocol://host/path only.
30
- * scope - a map of requested source -> scope pairs.
31
- * A scope is a space-separated list of capabilities.
32
- * iat - issued at time in absolute millis.
33
- * exp - expiry time, ditto.
34
- * sig - the base64 signature, verified by the JWK pointed to by iss.
35
- *
36
- * Normally the sub field must match the host portion of the iss URL. But
37
- * the possiblity remains open for an intermediary trusted by the party to
38
- * hold the public keys of counterparties.
39
- */
40
-
41
- /**
42
- * @typedef {object.<string, string>} Scope
43
- */
44
-
45
- /**
46
- * @typedef TokenPayload
47
- * @type {object<string, Token>}
48
- * @property {string} iss // URL of public key of counterparty making request.
49
- * @property {string} aud // Party origin handling the request.
50
- * @property {string} sub // Counterparty origin making the request.
51
- * @property {string} src // Source HTML document context of the request excluding fragment and query.
52
- * @property {Scope} scope // Map of scope (space-separated capabilities) keyed by source URL.
53
- * @property {number} iat // Issued at time.
54
- * @property {number} exp // Expiry time.
55
- *
56
- * @typedef SignedTokenPayload
57
- * @type {TokenPayload}
58
- * @property {string} sig // Counterparty signs, `iss` used to verify.
59
- */
60
-
61
- /**
62
- * @typedef Signatory
63
- * @type {object}
64
- * @property {string} role // Role label of this signatory.
65
- * @property {JSONWebKey} jwk // Public JWK of this signatory.
66
- */
67
-
68
- /**
69
- * @typedef TokenSet
70
- * @type {object<string, Token>}
71
- */
72
-
73
- /** Must match algorithm in Keypair.js */
74
- const ALGORITHM = {
75
- name: 'RSASSA-PKCS1-v1_5',
76
- hash: 'SHA-256'
77
- }
78
-
79
- /** Matches an origin with protocol and no path. */
80
- const MATCH_ORIGIN = /https?:\/\/[^\/]+$/
81
-
82
- /**
83
- * Allow a 30s leeway to allow for clock difference when checking
84
- * token issued at time. We allow our clock to be up to 30s behind
85
- * the system that generated a token.
86
- */
87
- const TOKEN_IAT_LEEWAY_MILLIS = 1000 * 30
88
-
89
- export class Token {
90
-
91
- // Standard sources.
92
- static Source = {
93
- PARTY_CONTROL: 'party:control' // Party control subject, a pseudo-source with scope granted to counterparties.
94
- }
95
-
96
- // Standard counterparty pattern.
97
- static Counterparty = {
98
- PUBLIC: 'public' // Public counterparty, matching requests with no host specified in sub.
99
- }
100
-
101
- // Standard signatory roles.
102
- static SignatoryRole = {
103
- PARTY: 'party',
104
- PARENT: 'parent'
105
- }
106
-
107
- // Standard scope labels.
108
- static Capability = {
109
- OPEN: 'open', // Capability to open source files and start tab runners.
110
- CLOSE: 'close', // Capability to close source files and stop tab runners.
111
- GRANT: 'grant', // Capability to create relation records.
112
- REVOKE: 'revoke', // Capability to delete relation records.
113
- OFFER: 'offer', // Capability to create a party offer for a device to claim.
114
- RETRACT: 'retract', // Capability to delete a party offer before it is claimed.
115
- DELETE: 'delete', // Capability to delete devices.
116
- CATALOG: 'catalog', // Capability to get sources, devices, alerts etc.
117
- ALIAS: 'alias', // Capability to set the alias for a counterparty host.
118
- UNALIAS: 'unalias', // Capability to unset the alias for a counterparty host.
119
- GET_ITEM: 'getitem', // Capability to get a storage item.
120
- SET_ITEM: 'setitem', // Capability to set (and remove) a storage item.
121
- ALERT: 'alert', // Capability to post and delete alerts.
122
- LOG: 'log', // Capability to view logs.
123
- READ: 'read', // Capability to read from drive.
124
- WRITE: 'write' // Capability to write to drive.
125
- }
126
-
127
- static DEFAULT_EXPIRY_MILLIS = 1000 * 60 * 60 * 24
128
- static TOKEN_HEADER = 'x-tabserver-token'
129
- static SCOPE_SEPARATOR = /[\s,;\|]+/ // Specification per String.split() function.
130
-
131
- /** @type{Token} */
132
- #payload = null
133
-
134
- /** @type{string} */
135
- #signatureBase64 = null
136
-
137
- /** @type{URL} */
138
- #audUrl
139
-
140
- /** @type{URL} */
141
- #subUrl
142
-
143
- /** @type{URL} */
144
- #srcUrl
145
-
146
- /** @type {Signatory} */
147
- #signatory // Set upon successful verification of signature.
148
-
149
-
150
- /**
151
- * Constructor takes either a signed base64 token, or a payload object.
152
- *
153
- * If successful, the payload and optionally the signature fields are
154
- * populated.
155
- *
156
- * @param {object | string} source a payload, or a base64 token
157
- */
158
- constructor(source) {
159
- let payload = null
160
-
161
- if (typeof source == 'object') {
162
- payload = source
163
- }
164
- else if (typeof source == 'string') {
165
- try {
166
- payload = JSON.parse(atob(source))
167
- }
168
- catch (_e) {
169
- throw 'Invalid token format'
170
- }
171
- }
172
- else {
173
- throw `Cannot construct token from ${typeof source}`
174
- }
175
-
176
- // Separate out the signature from the payload.
177
- this.#signatureBase64 = payload.sig
178
- delete payload.sig
179
- this.#payload = payload
180
-
181
- const {
182
- iss,
183
- aud,
184
- sub,
185
- src,
186
- scope,
187
- iat,
188
- exp
189
- } = payload
190
-
191
- // Check payload is complete.
192
- if ((!iss || !aud || !sub || !src || !scope || !iat || !exp)) {
193
- throw 'Token must include iss, aud, sub, src, scope, iat and exp'
194
- }
195
-
196
- // Check aud, sub and src fields are origins (i.e. proto://host.name)
197
- if (
198
- !aud.match(MATCH_ORIGIN) ||
199
- !sub.match(MATCH_ORIGIN)
200
- ) {
201
- throw 'The aud and sub attributes must be origins'
202
- }
203
-
204
- this.#audUrl = new URL(aud)
205
- this.#subUrl = new URL(sub)
206
-
207
- // The `src` attribute must not have query or fragment.
208
- if (
209
- src.includes('?') ||
210
- src.includes('#')
211
- ) {
212
- throw 'The src attribute must have no query or fragment component.'
213
- }
214
-
215
- this.#srcUrl = new URL(src)
216
-
217
- this.#payload = payload
218
- }
219
-
220
- /**
221
- * Generates the signature for the object using the private key provided by the
222
- * supplied callback and stores the result.
223
- *
224
- * @param {Promise<JsonWebKey>} privateJwkPromise the private key used for signing.
225
- * @return {Promise<string>} resolved with the base64 signature if signed successfully.
226
- */
227
- async signWith(privateJwkPromise) {
228
- if (this.#payload == null) {
229
- throw 'No payload to sign'
230
- }
231
-
232
- const privateJwk = await privateJwkPromise
233
- const privateKey = await crypto.subtle.importKey('jwk', privateJwk, ALGORITHM, true, ['sign'])
234
- const toSign = JSON.stringify(this.#payload)
235
- const messageBuffer = new TextEncoder().encode(toSign).buffer
236
- const signature = await crypto.subtle.sign(ALGORITHM, privateKey, messageBuffer)
237
- const signatureBase64 = Token.bytesToBase64(new Uint8Array(signature))
238
- this.#signatureBase64 = signatureBase64
239
- return signatureBase64
240
- }
241
-
242
- /**
243
- * Retrieves the signatory using the `iss` field, checks the signature
244
- * and if valid sets the signatory property in this instance.
245
- *
246
- * @throws {string} if signatory cannot be retrieved or signature doesn't match.
247
- */
248
- async verifySignatory() {
249
- await this.fetchSignatory()
250
- await this.checkSignature(Promise.resolve(this.#signatory.jwk))
251
- }
252
-
253
- /**
254
- * Fetches the signatory from the `iss` URL, setting the instance
255
- * property.
256
- *
257
- * The signatory comprises the `role` label and the public `jwk`.
258
- *
259
- * Caching may be introduced to reduce latency and bandwidth use.
260
- *
261
- * @throws {string} error if fetch fails or returns an error response.
262
- */
263
- async fetchSignatory() {
264
- const issuer = this.getIssuer()
265
- try {
266
- const response = await fetch(issuer, {
267
- headers: {
268
- 'content-type': 'application/json'
269
- }
270
- })
271
-
272
- const json = await response.json()
273
- if ('error' in json) {
274
- throw json.error
275
- }
276
- this.#signatory = json.ok
277
- }
278
- catch (_e) {
279
- throw `Failed to read signatory at ${issuer}`
280
- }
281
- }
282
-
283
- /**
284
- * Checks the signature using the public key provided by the supplied callback,
285
- * and stores the result.
286
- *
287
- * @param {Promise<JsonWebKey>} publicJwkPromise the public key used to verify the signature.
288
- * @throws {string} exception if signature cannot be verified or the public key is null or error.
289
- */
290
- async checkSignature(publicJwkPromise) {
291
- const publicJwk = await publicJwkPromise
292
- if (!publicJwk || 'error' in publicJwk) {
293
- throw publicJwk.error
294
- }
295
- const publicKey = await crypto.subtle.importKey('jwk', publicJwk, ALGORITHM, true, ['verify'])
296
- const toCheck = JSON.stringify(this.#payload)
297
- const messageBuffer = new TextEncoder().encode(toCheck).buffer
298
- const sigBuffer = Token.base64ToBytes(this.#signatureBase64).buffer
299
- const isVerified = await crypto.subtle.verify(ALGORITHM, publicKey, sigBuffer, messageBuffer)
300
- if (!isVerified) {
301
- throw 'Signature cannot be verified'
302
- }
303
- }
304
-
305
- /**
306
- * Returns the payload with the `sig` property added.
307
- *
308
- * @return {SignedTokenPayload} the payload with additional `sig` property.
309
- */
310
- getSignedPayload() {
311
- if (this.#signatureBase64 === null) {
312
- throw 'Not yet signed'
313
- }
314
-
315
- return {
316
- ...this.#payload,
317
- sig: this.#signatureBase64
318
- }
319
- }
320
-
321
- /**
322
- * Returns the payload whether signed or not.
323
- *
324
- * @return {TokenPayload} the payload object without signature.
325
- */
326
- getPayload() {
327
- return this.#payload
328
- }
329
-
330
- /**
331
- * Returns the `aud` field as a string.
332
- *
333
- * @return {string} the `aud` field value.
334
- */
335
- getAud() {
336
- return this.#audUrl.origin
337
- }
338
-
339
-
340
- /**
341
- * Returns the `aud` host which is the party name handling the request.
342
- *
343
- * @return {string} the party name.
344
- */
345
- getParty() {
346
- return this.#audUrl.host
347
- }
348
-
349
- /**
350
- * Returns the `sub` field as a string.
351
- *
352
- * @return {string} the `sub` field value.
353
- */
354
- getSub() {
355
- return this.#subUrl.origin
356
- }
357
-
358
- /**
359
- * Returns the `sub` host, which is the counterparty name making the
360
- * request.
361
- *
362
- * @return {string} the counterparty name.
363
- */
364
- getCounterparty() {
365
- return this.#subUrl.host
366
- }
367
-
368
- /**
369
- * Returns the role of the signatory who signed on behalf of the
370
- * counterparty.
371
- *
372
- * @returns {string} role of the verified signatory.
373
- */
374
- getSignatoryRole() {
375
- return this.#signatory.role
376
- }
377
-
378
- /**
379
- * Returns the `src` URL.
380
- *
381
- * @return {URL} the source URL.
382
- */
383
- getSourceUrl() {
384
- return this.#srcUrl
385
- }
386
-
387
- /**
388
- * Returns the `issuer` field as a URL. The counterparty is inferred from
389
- * the host name which generally matches the sub field.
390
- *
391
- * @return {URL} the issuer URL.
392
- */
393
- getIssuer() {
394
- return new URL(this.getPayload().iss)
395
- }
396
-
397
- /**
398
- * Returns the list of zero or more sources for which scope is requested.
399
- *
400
- * @return {string[]} a list of source URL strings.
401
- */
402
- getSources() {
403
- const sources = []
404
- for (const source in this.#payload.scope) {
405
- sources.push(source)
406
- }
407
- return sources
408
- }
409
-
410
- /**
411
- * Returns the scope for the source as an array of capability tokens.
412
- *
413
- * These can be separated by any mix of space, comma, semicolon
414
- * or vertical bar.
415
- *
416
- * @param {string} source whose capabilities are returned.
417
- * @return {string[]} zero or more scope tokens.
418
- */
419
- getCapabilities(source) {
420
- const scope = this.#payload.scope
421
- if (! (source in scope)) {
422
- throw `Source '${source}' not in scope`
423
- }
424
-
425
- return scope[source].split(Token.SCOPE_SEPARATOR)
426
- }
427
-
428
- /**
429
- * Returns true if the supplied capability is present in the token
430
- * scope under the specified source, otherwise false.
431
- *
432
- * @param {string} source the source under which the capability is expected.
433
- * @param {string} capability the capability being tested.
434
- * @return {boolean} true if capability is present in the scope, otherwise false.
435
- */
436
- hasCapability(source, capability) {
437
- return this.getCapabilities(source).includes(capability)
438
- }
439
-
440
- /**
441
- * Checks the token is within the period defined by the `iat` and `exp`
442
- * date fields.
443
- *
444
- * @throws {string} exception if not within valid period.
445
- */
446
- checkPeriod() {
447
- const {
448
- iat,
449
- exp
450
- } = this.#payload
451
-
452
- const now = Date.now()
453
- if (now < iat - TOKEN_IAT_LEEWAY_MILLIS) {
454
- throw 'Token is not yet valid'
455
- }
456
-
457
- if (now > exp) {
458
- throw 'Token has expired'
459
- }
460
- }
461
-
462
- /**
463
- * Returns the signed object including the `sig` property as a base64 string.
464
- *
465
- * @return {string} the signed object as a base64 string.
466
- */
467
- asSignedBase64() {
468
- return btoa(JSON.stringify(this.getSignedPayload()))
469
- }
470
-
471
- /**
472
- * Returns a base64 JSON string corresponding to the provided byte array.
473
- *
474
- * @param {Uint8Array} bytes to be encoded as a base64 string.
475
- * @returns {string} the encoded bytes.
476
- */
477
- static bytesToBase64(bytes) {
478
- const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join('');
479
- return btoa(binString);
480
- }
481
-
482
- /**
483
- * Returns the byte array decoded from the base64 string.
484
- *
485
- * @param {string} base64
486
- * @returns {Uint8Array} the decoded bytes.
487
- */
488
- static base64ToBytes(base64) {
489
- const binString = atob(base64);
490
- return Uint8Array.from(binString, (m) => m.codePointAt(0));
491
- }
492
-
493
- /**
494
- * Returns a new token using the x-tabserver-token header on the
495
- * request.
496
- *
497
- * @param {Request} the request.
498
- * @return {Token | null} the token, or null if no header is present.
499
- * @throws {string} exception if token cannot be constructed.
500
- */
501
- static from(request) {
502
- if (request.headers.has(Token.TOKEN_HEADER)) {
503
- return new Token(request.headers.get(Token.TOKEN_HEADER))
504
- }
505
- return null
506
- }
507
-
508
- /**
509
- * Converts an object whose string keys map to token objects
510
- * into URL-friendly search params suitable for use in a
511
- * query string or fragment.
512
- *
513
- * The string keys are normally the audience hostname, or the
514
- * special key 'party' which holds the identity token whose
515
- * `aud` and `sub` are the same.
516
- *
517
- * @param {TokenSet} tokenSet the set of tokens keyed by string.
518
- * @returns {string}
519
- */
520
- static toSearchString(tokenSet) {
521
- const searchParams = new URLSearchParams()
522
- for (const key in tokenSet) {
523
- const token = tokenSet[key]
524
- const tokenBase64 = token.asSignedBase64()
525
- searchParams.append(key, tokenBase64)
526
- }
527
- return searchParams.toString()
528
- }
529
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * Use this type to reference a plain object with
3
- * random string keys.
4
- */
5
- export interface Plain {
6
- // deno-lint-ignore no-explicit-any
7
- [key: string]: any
8
- }
9
-
10
- /**
11
- * Any JSON-serialisable type.
12
- */
13
- export type JSONPrimitive = string | number | boolean | null
14
- export type JSONArray = JSONValue[]
15
- export interface JSONObject {
16
- [key: string]: JSONValue
17
- }
18
- export type JSONValue = JSONPrimitive | JSONObject | JSONArray
19
-
20
-
21
- /**
22
- * Type predicates to be used in type-narrowing assertions, e.g.
23
- *
24
- * assert(notNull(someValue))
25
- * ... now the type of someValue does not include null ...
26
- *
27
- * or
28
- * if (isOk(returnValue)) {
29
- * ...in this block we know returnValue.ok is present and of the correct type.
30
- * }
31
- *
32
- * @param value the value to be tested.
33
- * @returns true if not null, otherwise false.
34
- */
35
- export function notNull<T>(value: T | null): value is T {
36
- return value !== null
37
- }
38
-
39
- export function isDefined<T>(value: T | undefined): value is T {
40
- return value !== undefined
41
- }