webdaemon 1.0.0 → 11.4.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/index.js CHANGED
@@ -1,8 +1,17 @@
1
- export * from './src/Alert.js'
2
- export * from './src/BrowserApp.js'
3
- export * from './src/Digest.js'
4
- export * from './src/KeyPair.js'
5
- export * from './src/ParentHelper.js'
6
- export * from './src/QrCode.js'
7
- export * from './src/Storage.js'
8
- export * from './src/Token.js'
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": "1.0.0",
4
- "description": "Web Daemon f/e",
3
+ "version": "11.4.0",
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",
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "webdaemon",
3
+ "version": "<FILLED IN>",
4
+ "description": "Web Daemon",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "es6",
8
+ "npm",
9
+ "library"
10
+ ],
11
+ "author": "Web Daemon Authors",
12
+ "license": "ISC"
13
+ }
@@ -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>
@@ -0,0 +1,4 @@
1
+ # Javascript library
2
+ This directory holds Javascript source files designed to be
3
+ used in either the browser or a daemon runner, served from
4
+ a public site such as https://webdaemon.online.
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>
@@ -0,0 +1,4 @@
1
+ # Typescript library
2
+ This directory holds Typescript source files designed to be
3
+ used in daemon runners only, and served from a public site
4
+ such as https://webdaemon.online.
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