wirejs-resources 0.1.10 → 0.1.12

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ Experimental.
2
+
3
+ If this package interests you, contact the author.
@@ -0,0 +1,14 @@
1
+ import type { Secret } from '../resources/secret.js';
2
+ import { Cookie, CookieJar } from './cookie-jar.js';
3
+ import { Resource } from '../resource.js';
4
+ export declare class SignedCookie<T> {
5
+ private scope;
6
+ private name;
7
+ private signingSecret;
8
+ private options;
9
+ constructor(scope: Resource | string, name: string, signingSecret: Secret, options?: Omit<Cookie, 'value' | 'name'>);
10
+ get cookieName(): string;
11
+ read(cookies: CookieJar): Promise<T | null>;
12
+ write(cookies: CookieJar, value: T | null): Promise<void>;
13
+ clear(cookies: CookieJar): void;
14
+ }
@@ -0,0 +1,66 @@
1
+ import * as jose from 'jose';
2
+ const ONE_HOUR = 60 * 60; // minutes/hour * seconds/minute
3
+ export class SignedCookie {
4
+ scope;
5
+ name;
6
+ signingSecret;
7
+ options;
8
+ constructor(scope, name, signingSecret, options = {}) {
9
+ this.scope = scope;
10
+ this.name = name;
11
+ this.signingSecret = signingSecret;
12
+ this.options = options;
13
+ }
14
+ get cookieName() {
15
+ if (typeof this.scope === 'string') {
16
+ return `${this.scope.replaceAll('/', '-')}-${this.name}`;
17
+ }
18
+ else {
19
+ return `${this.scope.absoluteId.replaceAll('/', '-')}-${this.name}`;
20
+ }
21
+ }
22
+ async read(cookies) {
23
+ try {
24
+ const cookie = cookies.get(this.cookieName)?.value;
25
+ const result = cookie ? await jose.jwtVerify(cookie, new TextEncoder().encode(await this.signingSecret.read())) : undefined;
26
+ if (result?.payload) {
27
+ return result.payload;
28
+ }
29
+ return null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ async write(cookies, value) {
36
+ if (!value) {
37
+ this.clear(cookies);
38
+ }
39
+ else {
40
+ const duration = typeof this.options.maxAge === 'number'
41
+ ? this.options.maxAge
42
+ : ONE_HOUR;
43
+ const httpOnly = typeof this.options.httpOnly === 'boolean'
44
+ ? this.options.httpOnly
45
+ : true;
46
+ const secure = typeof this.options.secure === 'boolean'
47
+ ? this.options.secure
48
+ : true;
49
+ const jwt = await new jose.SignJWT(value)
50
+ .setProtectedHeader({ alg: 'HS256' })
51
+ .setIssuedAt()
52
+ .setExpirationTime(`${duration}s`)
53
+ .sign(new TextEncoder().encode(await this.signingSecret.read()));
54
+ cookies.set({
55
+ name: this.cookieName,
56
+ value: jwt,
57
+ httpOnly,
58
+ secure,
59
+ maxAge: duration
60
+ });
61
+ }
62
+ }
63
+ clear(cookies) {
64
+ cookies.delete(this.cookieName);
65
+ }
66
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { FileService } from './services/file.js';
2
2
  export { AuthenticationService } from './services/authentication.js';
3
3
  export { CookieJar } from './adapters/cookie-jar.js';
4
- export { withContext, requiresContext, Context } from './adapters/context.js';
4
+ export { SignedCookie } from './adapters/signed-cookie.js';
5
+ export { withContext, requiresContext, Context, ContextWrapped } from './adapters/context.js';
5
6
  export { Resource } from './resource.js';
6
7
  export { overrides } from './overrides.js';
8
+ export { Secret } from './resources/secret.js';
7
9
  export * from './types.js';
8
10
  export * from './derived-types.js';
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  export { FileService } from './services/file.js';
2
2
  export { AuthenticationService } from './services/authentication.js';
3
3
  export { CookieJar } from './adapters/cookie-jar.js';
4
+ export { SignedCookie } from './adapters/signed-cookie.js';
4
5
  export { withContext, requiresContext, Context } from './adapters/context.js';
5
6
  export { Resource } from './resource.js';
6
7
  export { overrides } from './overrides.js';
8
+ export { Secret } from './resources/secret.js';
7
9
  export * from './types.js';
8
10
  export * from './derived-types.js';
@@ -1 +1 @@
1
- export declare function prebuildApi(): Promise<void>;
1
+ export declare function prebuildApi(apiDir: string): Promise<void>;
@@ -1,12 +1,10 @@
1
- import process from 'process';
2
1
  import fs from 'fs';
3
2
  import path from 'path';
4
- export async function prebuildApi() {
5
- const CWD = process.cwd();
3
+ export async function prebuildApi(apiDir) {
6
4
  let API_URL = '/api';
7
- const indexModule = await import(path.join(CWD, 'index.js'));
5
+ const indexModule = await import(path.join(apiDir, 'dist', 'index.js'));
8
6
  try {
9
- const backendConfigModule = await import(path.join(CWD, 'config.js'));
7
+ const backendConfigModule = await import(path.join(apiDir, 'config.js'));
10
8
  const backendConfig = backendConfigModule.default;
11
9
  console.log("backend config found", backendConfig);
12
10
  if (backendConfig.apiUrl) {
@@ -23,5 +21,5 @@ export async function prebuildApi() {
23
21
  `import { apiTree } from "wirejs-resources/client";`,
24
22
  `const INTERNAL_API_URL = ${JSON.stringify(API_URL)};`,
25
23
  ].join('\n');
26
- await fs.promises.writeFile(path.join(CWD, 'index.client.js'), [baseClient, apiCode].join('\n\n'));
24
+ await fs.promises.writeFile(path.join(apiDir, 'dist', 'index.client.js'), [baseClient, apiCode].join('\n\n'));
27
25
  }
@@ -1,21 +1,20 @@
1
1
  import { Resource } from '../resource.js';
2
+ import { ContextWrapped } from '../adapters/context.js';
2
3
  import type { CookieJar } from '../adapters/cookie-jar.js';
3
- import type { User, AuthenticationError, AuthenticationMachineInput, AuthenticationMachineState, AuthenticationServiceOptions, AuthenticationState } from '../types.js';
4
+ import type { User, AuthenticationError, AuthenticationMachineInput, AuthenticationMachineState, AuthenticationServiceOptions } from '../types.js';
4
5
  export declare class AuthenticationService extends Resource {
5
6
  #private;
6
7
  constructor(scope: Resource | string, id: string, { duration, keepalive, cookie }?: AuthenticationServiceOptions);
7
- getSigningSecret(): Promise<Uint8Array<ArrayBufferLike>>;
8
- get signingSecret(): Promise<Uint8Array<ArrayBufferLike>>;
9
- getState(cookies: CookieJar): Promise<AuthenticationState>;
8
+ private getState;
10
9
  getMachineState(cookies: CookieJar): Promise<AuthenticationMachineState>;
11
- setState(cookies: CookieJar, user?: User): Promise<void>;
10
+ private setState;
12
11
  missingFieldErrors(input: Record<string, string | number | boolean>, fields: string[]): AuthenticationError[] | undefined;
13
12
  setMachineState(cookies: CookieJar, form: AuthenticationMachineInput): Promise<AuthenticationMachineState | {
14
13
  errors: AuthenticationError[];
15
14
  }>;
16
- buildApi(this: AuthenticationService): import("../adapters/context.js").ContextfulApiNamespace<{
15
+ buildApi(): ContextWrapped<{
17
16
  getState: () => Promise<AuthenticationMachineState>;
18
- setState: (options: Parameters<(typeof this)["setMachineState"]>[1]) => Promise<AuthenticationMachineState | {
17
+ setState: (options: AuthenticationMachineInput) => Promise<AuthenticationMachineState | {
19
18
  errors: AuthenticationError[];
20
19
  }>;
21
20
  getCurrentUser: () => Promise<User | null>;
@@ -1,8 +1,8 @@
1
1
  import { scrypt, randomBytes, randomUUID } from 'crypto';
2
- import * as jose from 'jose';
3
2
  import { Resource } from '../resource.js';
4
3
  import { FileService } from './file.js';
5
4
  import { Secret } from '../resources/secret.js';
5
+ import { SignedCookie } from '../adapters/signed-cookie.js';
6
6
  import { withContext } from '../adapters/context.js';
7
7
  import { overrides } from '../overrides.js';
8
8
  function newId() {
@@ -181,60 +181,23 @@ class UserStore {
181
181
  }
182
182
  }
183
183
  export class AuthenticationService extends Resource {
184
- #duration;
185
- ;
186
184
  #keepalive;
187
- #cookieName;
188
- #rawSigningSecret;
189
- #signingSecret;
190
185
  #users;
186
+ #cookie;
191
187
  constructor(scope, id, { duration, keepalive, cookie } = {}) {
192
188
  super(scope, id);
193
- this.#duration = duration || ONE_WEEK;
194
189
  this.#keepalive = !!keepalive;
195
- this.#cookieName = cookie ?? 'identity';
196
- this.#rawSigningSecret = new (overrides.Secret || Secret)(this, 'jwt-signing-secret');
190
+ const signingSecret = new (overrides.Secret || Secret)(this, 'jwt-signing-secret');
197
191
  const fileService = new (overrides.FileService || FileService)(this, 'files');
192
+ this.#cookie = new SignedCookie(this, cookie ?? 'identity', signingSecret, { maxAge: ONE_WEEK });
198
193
  this.#users = new UserStore(fileService);
199
194
  }
200
- async getSigningSecret() {
201
- const secretAsString = await this.#rawSigningSecret.read();
202
- return new TextEncoder().encode(secretAsString);
203
- }
204
- get signingSecret() {
205
- if (!this.#signingSecret) {
206
- this.#signingSecret = this.getSigningSecret();
207
- }
208
- return this.#signingSecret;
209
- }
210
195
  async getState(cookies) {
211
- let idCookie;
212
- let user;
213
- try {
214
- idCookie = cookies.get(this.#cookieName)?.value;
215
- const idPayload = idCookie ? (await jose.jwtVerify(idCookie, await this.signingSecret)) : undefined;
216
- user = idPayload ? {
217
- id: idPayload.payload.sub,
218
- username: idPayload.payload.username,
219
- displayName: idPayload.payload.username,
220
- } : undefined;
221
- }
222
- catch (err) {
223
- // jose doesn't like our cookie.
224
- console.error(err);
225
- }
226
- if (user) {
227
- return {
228
- state: 'authenticated',
229
- user
230
- };
231
- }
232
- else {
233
- return {
234
- state: 'unauthenticated',
235
- user: undefined,
236
- };
237
- }
196
+ const cookieState = await this.#cookie.read(cookies);
197
+ return cookieState ? cookieState : {
198
+ state: 'unauthenticated',
199
+ user: undefined
200
+ };
238
201
  }
239
202
  async getMachineState(cookies) {
240
203
  const state = await this.getState(cookies);
@@ -261,21 +224,12 @@ export class AuthenticationService extends Resource {
261
224
  }
262
225
  async setState(cookies, user) {
263
226
  if (!user) {
264
- cookies.delete(this.#cookieName);
227
+ this.#cookie.clear(cookies);
265
228
  }
266
229
  else {
267
- const jwt = await new jose.SignJWT(user)
268
- .setProtectedHeader({ alg: 'HS256' })
269
- .setIssuedAt()
270
- .setSubject(user.id)
271
- .setExpirationTime(`${this.#duration}s`)
272
- .sign(await this.signingSecret);
273
- cookies.set({
274
- name: this.#cookieName,
275
- value: jwt,
276
- httpOnly: true,
277
- secure: true,
278
- maxAge: this.#duration
230
+ return this.#cookie.write(cookies, {
231
+ state: 'authenticated',
232
+ user
279
233
  });
280
234
  }
281
235
  }
package/dist/types.d.ts CHANGED
@@ -27,8 +27,11 @@ export type AuthenticationAction = {
27
27
  buttons?: readonly string[];
28
28
  };
29
29
  export type AuthenticationState = {
30
- state: 'authenticated' | 'unauthenticated';
31
- user: User | undefined;
30
+ state: 'authenticated';
31
+ user: User;
32
+ } | {
33
+ state: 'unauthenticated';
34
+ user: undefined;
32
35
  };
33
36
  export type AuthenticationMachineAction = Readonly<AuthenticationAction & {
34
37
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-resources",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Basic services and server-side resources for wirejs apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "files": [
42
42
  "package.json",
43
+ "README.md",
43
44
  "dist/*"
44
45
  ]
45
- }
46
+ }