webdaemon 1.0.0 → 11.4.1
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/index.js +17 -8
- package/package.json +2 -5
- package/package.json.template +13 -0
- package/src/{BrowserApp.js → js/BrowserApp.js} +0 -10
- package/src/js/README.html +13 -0
- package/src/js/README.md +4 -0
- package/src/js/README.pdf +0 -0
- package/src/{Storage.js → js/Storage.js} +1 -28
- package/src/ts/Assertions.ts +41 -0
- package/src/ts/Lifecycle.ts +307 -0
- package/src/ts/README.html +13 -0
- package/src/ts/README.md +4 -0
- package/src/ts/README.pdf +0 -0
- package/src/ts/Requests.ts +131 -0
- package/src/ts/Responses.ts +132 -0
- /package/src/{Alert.js → js/Alert.js} +0 -0
- /package/src/{Digest.js → js/Digest.js} +0 -0
- /package/src/{KeyPair.js → js/KeyPair.js} +0 -0
- /package/src/{ParentHelper.js → js/ParentHelper.js} +0 -0
- /package/src/{QrCode.js → js/QrCode.js} +0 -0
- /package/src/{Token.js → js/Token.js} +0 -0
package/index.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './src/
|
|
3
|
-
export * from './src/
|
|
4
|
-
export * from './src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export * from './src/
|
|
8
|
-
export * from './src/
|
|
1
|
+
/** Common libs */
|
|
2
|
+
export * from './src/js/Digest.js'
|
|
3
|
+
export * from './src/js/KeyPair.js'
|
|
4
|
+
export * from './src/js/Token.js'
|
|
5
|
+
|
|
6
|
+
/** Browser libs */
|
|
7
|
+
export * from './src/js/Alert.js'
|
|
8
|
+
export * from './src/js/BrowserApp.js'
|
|
9
|
+
export * from './src/js/ParentHelper.js'
|
|
10
|
+
export * from './src/js/QrCode.js'
|
|
11
|
+
export * from './src/js/Storage.js'
|
|
12
|
+
|
|
13
|
+
/** Backend libs */
|
|
14
|
+
export * from './src/ts/Assertions.ts'
|
|
15
|
+
export * from './src/ts/Lifecycle.ts'
|
|
16
|
+
export * from './src/ts/Requests.ts'
|
|
17
|
+
export * from './src/ts/Responses.ts'
|
package/package.json
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webdaemon",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Web Daemon
|
|
3
|
+
"version": "11.4.1",
|
|
4
|
+
"description": "Web Daemon",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
},
|
|
9
6
|
"keywords": [
|
|
10
7
|
"es6",
|
|
11
8
|
"npm",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Token } from './Token.js'
|
|
2
|
-
import { Storage } from './Storage.js'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* @module
|
|
@@ -332,13 +331,4 @@ export class BrowserApp {
|
|
|
332
331
|
throw json.error
|
|
333
332
|
}
|
|
334
333
|
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Gets a Storage instance using the party token.
|
|
338
|
-
*
|
|
339
|
-
* @returns {Storage} instance.
|
|
340
|
-
*/
|
|
341
|
-
getStorage() {
|
|
342
|
-
return new Storage(this.getToken())
|
|
343
|
-
}
|
|
344
334
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Readme</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1 id="javascript-library">Javascript library</h1>
|
|
9
|
+
<p>This directory holds Javascript source files designed to be used in
|
|
10
|
+
either the browser or a daemon runner, served from a public site such as
|
|
11
|
+
https://webdaemon.online.</p>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/src/js/README.md
ADDED
|
Binary file
|
|
@@ -25,34 +25,6 @@ const KEYLIKE_PATTERN = /^[\w\-.%_]+(?:\/[\w\-.%_]+)*$/
|
|
|
25
25
|
* @property {string} error
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* An instance of this class is normally obtained from BrowserApp, which constructs
|
|
30
|
-
* the instance using the party token.
|
|
31
|
-
*/
|
|
32
|
-
export class Storage {
|
|
33
|
-
#token // Party token which must include getitem and setitem capabilities on party:control.
|
|
34
|
-
|
|
35
|
-
constructor(token) {
|
|
36
|
-
this.#token = token
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getItem(key) {
|
|
40
|
-
return getItem(this.#token, key)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getItemsLike(keylike) {
|
|
44
|
-
return getItemsLike(this.#token, keylike)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
setItem(key, value) {
|
|
48
|
-
return setItem(this.#token, key, value)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
removeItem(key) {
|
|
52
|
-
return removeItem(this.#token, key)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
28
|
/**
|
|
57
29
|
* Throws an exception if the key being used is not
|
|
58
30
|
* in a valid format.
|
|
@@ -153,6 +125,7 @@ export async function getItemsLike(token, keylike) {
|
|
|
153
125
|
assertValidLike(keylike)
|
|
154
126
|
const url = new URL(`${token.getAud()}/storage`)
|
|
155
127
|
url.searchParams.append('like', keylike)
|
|
128
|
+
console.log(url)
|
|
156
129
|
const response = await fetch(url, {
|
|
157
130
|
method: 'GET',
|
|
158
131
|
headers: {
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { response } from './Responses.ts'
|
|
2
|
+
import { Plain, JSONValue } from './Assertions.ts'
|
|
3
|
+
import { KeyPair } from '../js/KeyPair.js'
|
|
4
|
+
import { Token, Scope } from '../js/Token.js'
|
|
5
|
+
|
|
6
|
+
// Daemon configuration looks like this.
|
|
7
|
+
export interface DaemonConfig {
|
|
8
|
+
system: {
|
|
9
|
+
protocol: 'http:' | 'https:'
|
|
10
|
+
party: string
|
|
11
|
+
issuer: string
|
|
12
|
+
source: string
|
|
13
|
+
sourcePrefix: string
|
|
14
|
+
}
|
|
15
|
+
user: Plain
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// System requests are on the /daemon/... path.
|
|
19
|
+
export const DAEMON_PREFIX = 'daemon'
|
|
20
|
+
|
|
21
|
+
// Daemon lifecycle path components that come after DAEMON_PREFIX.
|
|
22
|
+
export const CONFIG_PATH = 'config'
|
|
23
|
+
export const EVENT_PATH = 'event'
|
|
24
|
+
export const PUBLIC_JWK_PATH = 'public.jwk'
|
|
25
|
+
|
|
26
|
+
// SessionStorage keys for lifecycle objects.
|
|
27
|
+
const CONFIG_KEY = 'config'
|
|
28
|
+
const PRIVATE_JWK_KEY = 'privateJwk'
|
|
29
|
+
const PUBLIC_JWK_KEY = 'publicJwk'
|
|
30
|
+
|
|
31
|
+
// Default time-to-live for a third party token we generate is 1 hour.
|
|
32
|
+
const DEFAULT_TTL_MILLIS = 1000 * 60 * 60
|
|
33
|
+
|
|
34
|
+
interface TabEvent {
|
|
35
|
+
type: string,
|
|
36
|
+
payload: JSONValue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Encapsulates the lifecycle event requests that occur on
|
|
41
|
+
* runner start and termination and when alerts are resolved
|
|
42
|
+
* or rejected.
|
|
43
|
+
*
|
|
44
|
+
* The Lifecycle object also provides static methods to retrieve
|
|
45
|
+
* configuration items, a public and private key pair, and also to
|
|
46
|
+
* produce the signed token that is required in requests from this party
|
|
47
|
+
* to third parties.
|
|
48
|
+
*/
|
|
49
|
+
export class Lifecycle extends EventTarget {
|
|
50
|
+
static #instance: Lifecycle = new Lifecycle()
|
|
51
|
+
|
|
52
|
+
static getInstance() {
|
|
53
|
+
return this.#instance
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* All system requests are under the /daemon/ path.
|
|
58
|
+
*
|
|
59
|
+
* @param {Request} request to be tested.
|
|
60
|
+
* @returns {boolean} true if the request is a lifecycle request.
|
|
61
|
+
*/
|
|
62
|
+
static shouldHandle(request: Request): boolean {
|
|
63
|
+
return (
|
|
64
|
+
Lifecycle.isConfig(request) ||
|
|
65
|
+
Lifecycle.isEvent(request) ||
|
|
66
|
+
Lifecycle.isPublicJwk(request)
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static isConfig(request: Request): boolean {
|
|
71
|
+
const url = new URL(request.url)
|
|
72
|
+
return (
|
|
73
|
+
request.method == 'POST' &&
|
|
74
|
+
url.pathname == `/${DAEMON_PREFIX}/${CONFIG_PATH}` &&
|
|
75
|
+
request.headers.get('Content-Type') == 'application/json'
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static isEvent(request: Request): boolean {
|
|
80
|
+
const url = new URL(request.url)
|
|
81
|
+
return (
|
|
82
|
+
request.method == 'POST' &&
|
|
83
|
+
url.pathname == `/${DAEMON_PREFIX}/${EVENT_PATH}` &&
|
|
84
|
+
request.headers.get('Content-Type') == 'application/json'
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static isPublicJwk(request: Request): boolean {
|
|
89
|
+
const url = new URL(request.url)
|
|
90
|
+
return (
|
|
91
|
+
request.method == 'GET' &&
|
|
92
|
+
url.pathname == `/${DAEMON_PREFIX}/${PUBLIC_JWK_PATH}`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handles a lifecycle request.
|
|
98
|
+
*
|
|
99
|
+
* @param {Request} request to be handled.
|
|
100
|
+
* @returns {Response} response for caller.
|
|
101
|
+
*/
|
|
102
|
+
async handler(request: Request): Promise<Response> {
|
|
103
|
+
if (Lifecycle.isConfig(request)) {
|
|
104
|
+
return await this.handleConfig(request)
|
|
105
|
+
}
|
|
106
|
+
if (Lifecycle.isEvent(request)) {
|
|
107
|
+
return await this.handleEvent(request)
|
|
108
|
+
}
|
|
109
|
+
if (Lifecycle.isPublicJwk(request)) {
|
|
110
|
+
return await this.handlePublicJwk()
|
|
111
|
+
}
|
|
112
|
+
return response({
|
|
113
|
+
error: 'Invalid daemon request'
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Saves the lifecycle config object provided in the request body.
|
|
119
|
+
*
|
|
120
|
+
* Fires a 'config' lifecycle event.
|
|
121
|
+
*
|
|
122
|
+
* @param {Request} request containing the configuration json.
|
|
123
|
+
* @return {Response} ok response.
|
|
124
|
+
*/
|
|
125
|
+
async handleConfig(request: Request): Promise<Response> {
|
|
126
|
+
const configJson = await request.json()
|
|
127
|
+
sessionStorage.setItem(CONFIG_KEY, JSON.stringify(configJson))
|
|
128
|
+
this.dispatchEvent(new CustomEvent('config', {
|
|
129
|
+
detail: configJson
|
|
130
|
+
}))
|
|
131
|
+
return response({
|
|
132
|
+
ok: true
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Fires a lifecycle event upon receiving an event request.
|
|
138
|
+
*
|
|
139
|
+
* @param {Request} request containing the event.
|
|
140
|
+
* @return {Response} ok response.
|
|
141
|
+
*/
|
|
142
|
+
async handleEvent(request: Request): Promise<Response> {
|
|
143
|
+
const {
|
|
144
|
+
type,
|
|
145
|
+
payload
|
|
146
|
+
} = await request.json()
|
|
147
|
+
this.dispatchEvent(new CustomEvent(type, {
|
|
148
|
+
detail: payload
|
|
149
|
+
}))
|
|
150
|
+
return response({
|
|
151
|
+
ok: true
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Responds with the JSON public key used to verify tokens
|
|
157
|
+
* produced by this instance.
|
|
158
|
+
*
|
|
159
|
+
* @return {Response} the response containing the Signatory (role, jwk).
|
|
160
|
+
*/
|
|
161
|
+
async handlePublicJwk(): Promise<Response> {
|
|
162
|
+
const role = 'party'
|
|
163
|
+
const jwk = await Lifecycle.getPublicKey()
|
|
164
|
+
return Response.json({
|
|
165
|
+
ok: {
|
|
166
|
+
role,
|
|
167
|
+
jwk
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Returns the configuration object stored on initialisation.
|
|
174
|
+
* @return {DaemonConfig} the daemon configuration object.
|
|
175
|
+
* @throws {string} exception if config is not yet initialised.
|
|
176
|
+
*/
|
|
177
|
+
static getConfig(): DaemonConfig {
|
|
178
|
+
const json = sessionStorage.getItem(CONFIG_KEY)
|
|
179
|
+
if (!json) {
|
|
180
|
+
throw 'DaemonConfig not ready'
|
|
181
|
+
}
|
|
182
|
+
return JSON.parse(json)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generates if necessary and returns a private key for
|
|
187
|
+
* use in signing tokens.
|
|
188
|
+
*
|
|
189
|
+
* @returns {JSONWebKey} the private key for this session.
|
|
190
|
+
*/
|
|
191
|
+
static async getPrivateKey(): Promise<JsonWebKey> {
|
|
192
|
+
const jsonWebKey = sessionStorage.getItem(PRIVATE_JWK_KEY)
|
|
193
|
+
if (!jsonWebKey) {
|
|
194
|
+
const keyPair = await Lifecycle.generateKeys()
|
|
195
|
+
return keyPair.privateJwk()
|
|
196
|
+
}
|
|
197
|
+
return JSON.parse(jsonWebKey)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generates if necessary and returns a public key for
|
|
202
|
+
* use by third parties to verifying tokens we generate.
|
|
203
|
+
*
|
|
204
|
+
* @returns {JSONWebKey} the public key for this session.
|
|
205
|
+
*/
|
|
206
|
+
static async getPublicKey(): Promise<JsonWebKey> {
|
|
207
|
+
const jsonWebKey = sessionStorage.getItem(PUBLIC_JWK_KEY)
|
|
208
|
+
if (!jsonWebKey) {
|
|
209
|
+
const keyPair = await Lifecycle.generateKeys()
|
|
210
|
+
return keyPair.publicJwk()
|
|
211
|
+
}
|
|
212
|
+
return JSON.parse(jsonWebKey)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Generates and stores the serialised public and private JWK keys in
|
|
217
|
+
* sessionStorage which survives the life of this process instance.
|
|
218
|
+
*
|
|
219
|
+
* @returns {KeyPair} keyPair as generated and stored in sessionStorage.
|
|
220
|
+
*/
|
|
221
|
+
static async generateKeys(): Promise<KeyPair> {
|
|
222
|
+
const keyPair = new KeyPair()
|
|
223
|
+
await keyPair.generate()
|
|
224
|
+
const privateJwk = await keyPair.privateJwk()
|
|
225
|
+
const publicJwk = await keyPair.publicJwk()
|
|
226
|
+
sessionStorage.setItem(PRIVATE_JWK_KEY, JSON.stringify(privateJwk))
|
|
227
|
+
sessionStorage.setItem(PUBLIC_JWK_KEY, JSON.stringify(publicJwk))
|
|
228
|
+
return keyPair
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Returns a token suitable for the `x-tabserver-token` header
|
|
233
|
+
* in a request to the supplied party with the required
|
|
234
|
+
* scope.
|
|
235
|
+
*
|
|
236
|
+
* If party is omitted, it defaults to this party.
|
|
237
|
+
* If
|
|
238
|
+
*
|
|
239
|
+
* @param {Scope} scope map of string source -> space-separated capabilities.
|
|
240
|
+
* @param {string=} party the party host name to whom the request will be sent.
|
|
241
|
+
* @param { src=} src the HTML page from which the request is (actually or notionally) made.
|
|
242
|
+
* @param {number=} ttlMillis the lifetime of this token.
|
|
243
|
+
* @returns {Token} the signed token.
|
|
244
|
+
* @throws {string} if configuration not yet initialised.
|
|
245
|
+
*
|
|
246
|
+
*/
|
|
247
|
+
static async getTokenFor(
|
|
248
|
+
scope: Scope,
|
|
249
|
+
party: string | null = null,
|
|
250
|
+
src: string | null = null,
|
|
251
|
+
ttlMillis: number = DEFAULT_TTL_MILLIS
|
|
252
|
+
): Promise<Token> {
|
|
253
|
+
const config = Lifecycle.getConfig()
|
|
254
|
+
if (!config) {
|
|
255
|
+
throw 'Lifecycle configuration not yet available'
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Extract protocol, our party, issuer and source from system config.
|
|
259
|
+
const {
|
|
260
|
+
system: {
|
|
261
|
+
protocol,
|
|
262
|
+
party: us,
|
|
263
|
+
issuer,
|
|
264
|
+
source
|
|
265
|
+
}
|
|
266
|
+
} = config
|
|
267
|
+
|
|
268
|
+
// Token src excludes query params.
|
|
269
|
+
const appSourceUrl = new URL(source)
|
|
270
|
+
|
|
271
|
+
const aud = party ? `${protocol}//${party}`: `${protocol}//${us}`
|
|
272
|
+
const sub = `${protocol}//${us}`
|
|
273
|
+
const now = Date.now()
|
|
274
|
+
|
|
275
|
+
const payload = {
|
|
276
|
+
iss: issuer,
|
|
277
|
+
aud,
|
|
278
|
+
sub,
|
|
279
|
+
src: src ?? `${appSourceUrl.origin}${appSourceUrl.pathname}`,
|
|
280
|
+
scope,
|
|
281
|
+
iat: now,
|
|
282
|
+
exp: now + ttlMillis
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const token = new Token(payload)
|
|
286
|
+
await token.signWith(Lifecycle.getPrivateKey())
|
|
287
|
+
return token
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Returns a token for our party with the setitem and getitem
|
|
292
|
+
* capabilities on the party control source.
|
|
293
|
+
*/
|
|
294
|
+
static getStorageToken(): Promise<Token> {
|
|
295
|
+
const {
|
|
296
|
+
system: {
|
|
297
|
+
party,
|
|
298
|
+
}
|
|
299
|
+
} = Lifecycle.getConfig()
|
|
300
|
+
|
|
301
|
+
const scope = {
|
|
302
|
+
[Token.Source.PARTY_CONTROL]: `${Token.Capability.GET_ITEM} ${Token.Capability.SET_ITEM}`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return Lifecycle.getTokenFor(scope, party)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Readme</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1 id="typescript-library">Typescript library</h1>
|
|
9
|
+
<p>This directory holds Typescript source files designed to be used in
|
|
10
|
+
daemon runners only, and served from a public site such as
|
|
11
|
+
https://webdaemon.online.</p>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/src/ts/README.md
ADDED
|
Binary file
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Plain } from "./Assertions.ts";
|
|
2
|
+
|
|
3
|
+
// Standard HTTP headers.
|
|
4
|
+
const X_FORWARDED_HOST = 'x-forwarded-host'
|
|
5
|
+
const X_FORWARDED_PROTO = 'x-forwarded-proto'
|
|
6
|
+
|
|
7
|
+
// Non-standard HTTP header used by tabserver.
|
|
8
|
+
const X_FORWARDED_PATHNAME = 'x-forwarded-pathname'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the request protocol. This is the first of the following:
|
|
12
|
+
*
|
|
13
|
+
* 1. The `x-forwarded-proto` header, if present, with trailing colon.
|
|
14
|
+
* 2. The request URL protocol, ditto.
|
|
15
|
+
*
|
|
16
|
+
* @param request the request.
|
|
17
|
+
* @returns {string} the protocol with trailing colon.
|
|
18
|
+
*/
|
|
19
|
+
export function getClientProtocol(request: Request): string {
|
|
20
|
+
if (request.headers.has(X_FORWARDED_PROTO)) {
|
|
21
|
+
return (request.headers.get(X_FORWARDED_PROTO) as string) + ':'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return new URL(request.url).protocol
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the request host name. This is the first of the following:
|
|
29
|
+
*
|
|
30
|
+
* 1. The `x-forwarded-host` header, if present.
|
|
31
|
+
* 2. The request URL hostname (including port, if present)
|
|
32
|
+
*
|
|
33
|
+
* @param request the request.
|
|
34
|
+
* @returns the protocol.
|
|
35
|
+
*/
|
|
36
|
+
export function getClientHost(request: Request): string {
|
|
37
|
+
if (request.headers.has(X_FORWARDED_HOST)) {
|
|
38
|
+
return request.headers.get(X_FORWARDED_HOST) || ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return new URL(request.url).host
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the request pathname. This is the first of the following:
|
|
46
|
+
*
|
|
47
|
+
* 1. The `x-forwarded-pathname` header, if present.
|
|
48
|
+
* 2. The request URL pathname.
|
|
49
|
+
*/
|
|
50
|
+
export function getClientPathname(request: Request): string {
|
|
51
|
+
if (request.headers.has(X_FORWARDED_PATHNAME)) {
|
|
52
|
+
return request.headers.get(X_FORWARDED_PATHNAME) || ''
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new URL(request.url).pathname
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the client request protocol, host and pathname components
|
|
60
|
+
* only.
|
|
61
|
+
*
|
|
62
|
+
* @param request the request.
|
|
63
|
+
* @return {URL} the protocol://host/pathname
|
|
64
|
+
*/
|
|
65
|
+
export function getClientUrlPhp(request: Request): URL {
|
|
66
|
+
const protocol = getClientProtocol(request)
|
|
67
|
+
const host = getClientHost(request)
|
|
68
|
+
const pathname = getClientPathname(request)
|
|
69
|
+
return new URL(`${protocol}//${host}${pathname}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the URL as requested by the client taking account
|
|
74
|
+
* of the x-forwarded protocol and host.
|
|
75
|
+
*
|
|
76
|
+
* @param request the request.
|
|
77
|
+
* @returns the URL.
|
|
78
|
+
*/
|
|
79
|
+
export function getClientUrl(request: Request): URL {
|
|
80
|
+
const originalUrl = new URL(request.url)
|
|
81
|
+
const newUrl = getClientUrlPhp(request)
|
|
82
|
+
newUrl.search = originalUrl.search
|
|
83
|
+
newUrl.hash = originalUrl.hash
|
|
84
|
+
return newUrl
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns the pathname portion of the request URL, excluding
|
|
89
|
+
* query and fragment components.
|
|
90
|
+
*
|
|
91
|
+
* @param {request} the request.
|
|
92
|
+
* @returns {string} the pathname which starts with a forward slash.
|
|
93
|
+
*/
|
|
94
|
+
export function getPathname(request: Request): string {
|
|
95
|
+
const url = new URL(request.url)
|
|
96
|
+
return url.pathname
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns the string value of a named cookie, or null if it is not
|
|
101
|
+
* present in the request.
|
|
102
|
+
*
|
|
103
|
+
* @param {Request} request the request.
|
|
104
|
+
* @param {string} cookieName the name of the cookie
|
|
105
|
+
* @returns {string} the cookie value, or null if not present.
|
|
106
|
+
*/
|
|
107
|
+
export function getCookie(request: Request, cookieName: string): string | null {
|
|
108
|
+
const cookies = getCookies(request)
|
|
109
|
+
if (cookieName in cookies) {
|
|
110
|
+
return cookies[cookieName]
|
|
111
|
+
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns a map of request cookie names to values, which can be empty.
|
|
117
|
+
*
|
|
118
|
+
* @param {Request} request the request.
|
|
119
|
+
* @returns {Plain} the plain object with zero or more cookie keys.
|
|
120
|
+
*/
|
|
121
|
+
export function getCookies(request: Request): Plain {
|
|
122
|
+
const cookies: Plain = {}
|
|
123
|
+
const headerValue = request.headers.get('Cookie')
|
|
124
|
+
if (headerValue) {
|
|
125
|
+
for (const keyVal of headerValue.split('; ')) {
|
|
126
|
+
const [key, value] = keyVal.split('=')
|
|
127
|
+
cookies[key] = value
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return cookies
|
|
131
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Plain } from './Assertions.ts'
|
|
2
|
+
|
|
3
|
+
export interface Ok<T> {
|
|
4
|
+
ok: T
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Error {
|
|
8
|
+
error: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns an ok response or an error response corresponding
|
|
13
|
+
* to the standard ok or error return value.
|
|
14
|
+
*
|
|
15
|
+
* If the error string starts with a space-separated status code,
|
|
16
|
+
* then that is used as the status code for the response.
|
|
17
|
+
*
|
|
18
|
+
* For example:
|
|
19
|
+
*
|
|
20
|
+
* {error: 'Ordinary Error'}
|
|
21
|
+
* is sent as is with status 200.
|
|
22
|
+
* {error: '401 Authentication Error'}
|
|
23
|
+
* is sent as {error: 'Authentication Error'} with status 401
|
|
24
|
+
*/
|
|
25
|
+
export function response<T>(rvalue: Ok<T> | Error, extraHeaders?: Plain) {
|
|
26
|
+
|
|
27
|
+
if ('ok' in rvalue) {
|
|
28
|
+
const body = JSON.stringify(rvalue)
|
|
29
|
+
return new Response(body, {
|
|
30
|
+
status: 200,
|
|
31
|
+
headers: {
|
|
32
|
+
...extraHeaders,
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Access-Control-Allow-Origin': '*',
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const error = rvalue.error
|
|
40
|
+
const [, status, message] = error.match(/(\d{3})\s(.+)/) ?? [null, '200', error]
|
|
41
|
+
const body = JSON.stringify({
|
|
42
|
+
error: message
|
|
43
|
+
})
|
|
44
|
+
return new Response(body, {
|
|
45
|
+
status: Number(status),
|
|
46
|
+
headers: {
|
|
47
|
+
...extraHeaders,
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
'Access-Control-Allow-Origin': '*'
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a 404 error response, including
|
|
56
|
+
* the standard JSON error return value.
|
|
57
|
+
* @deprecated Use response instead using a status-prefixed error message.
|
|
58
|
+
*/
|
|
59
|
+
export function response404<T>(json: Error = {
|
|
60
|
+
error: '404 Not Found'
|
|
61
|
+
}) {
|
|
62
|
+
const body = JSON.stringify(json)
|
|
63
|
+
return new Response(body, {
|
|
64
|
+
status: 404,
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'Access-Control-Allow-Origin': '*',
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns a 302 permanent redirect response.
|
|
74
|
+
* @param url the url to redirect to.
|
|
75
|
+
* @returns Response object.
|
|
76
|
+
*/
|
|
77
|
+
export function response302(url: URL, extraHeaders: Plain = []): Response {
|
|
78
|
+
return new Response(null, {
|
|
79
|
+
status: 302,
|
|
80
|
+
headers: {
|
|
81
|
+
'Location': url.toString(),
|
|
82
|
+
'Access-Control-Allow-Origin': '*',
|
|
83
|
+
...extraHeaders
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Simple build-a-cookie support.
|
|
90
|
+
*
|
|
91
|
+
* If the `expires` attribute is not defined, it's a session cookie.
|
|
92
|
+
* Otherwise it's a permanent cookie. To delete a cookie, use the
|
|
93
|
+
* `expires` attribute set to `new Date(0)`.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
export interface CookiePayload {
|
|
97
|
+
name: string
|
|
98
|
+
value: string
|
|
99
|
+
domain?: string
|
|
100
|
+
sameSite?: 'Strict' | 'Lax' | 'None'
|
|
101
|
+
path?: string
|
|
102
|
+
expires?: Date
|
|
103
|
+
secure?: boolean
|
|
104
|
+
httpOnly?: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type CookieString = string
|
|
108
|
+
|
|
109
|
+
export function buildCookie(payload: CookiePayload): CookieString {
|
|
110
|
+
const {
|
|
111
|
+
name,
|
|
112
|
+
value,
|
|
113
|
+
domain,
|
|
114
|
+
sameSite,
|
|
115
|
+
path = '/',
|
|
116
|
+
expires,
|
|
117
|
+
secure = true,
|
|
118
|
+
httpOnly = true
|
|
119
|
+
} = payload
|
|
120
|
+
|
|
121
|
+
const builder: string[] = []
|
|
122
|
+
builder.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
|
|
123
|
+
if (domain) builder.push(`Domain=${domain}`)
|
|
124
|
+
if (sameSite) builder.push(`SameSite=${sameSite}`)
|
|
125
|
+
builder.push(`Path=${path}`)
|
|
126
|
+
if (expires) builder.push(`Expires=${expires.toUTCString()}`)
|
|
127
|
+
if (secure) builder.push(`Secure`)
|
|
128
|
+
if (httpOnly) builder.push('HttpOnly')
|
|
129
|
+
|
|
130
|
+
const cookie = builder.join('; ')
|
|
131
|
+
return cookie
|
|
132
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|