@aooth/user 0.1.77 → 0.1.79

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/dist/index.cjs CHANGED
@@ -137,9 +137,17 @@ const DEFAULTS = {
137
137
  saltLength: 32
138
138
  };
139
139
  const PREFIX = "$scrypt$";
140
+ /** Node's default scrypt memory cap (32 MiB) — kept as the floor. */
141
+ const DEFAULT_MAXMEM = 32 * 1024 * 1024;
142
+ /** Upper bound for the memory a (possibly stored, untrusted) parameter set may ask for. */
143
+ const MAX_MAXMEM = 1024 * 1024 * 1024;
140
144
  function scryptAsync(password, salt, keyLength, options) {
145
+ const maxmem = Math.min(MAX_MAXMEM, Math.max(DEFAULT_MAXMEM, 256 * options.N * options.r + 1024 * 1024));
141
146
  return new Promise((resolve, reject) => {
142
- (0, node_crypto.scrypt)(password, salt, keyLength, options, (err, derived) => {
147
+ (0, node_crypto.scrypt)(password, salt, keyLength, {
148
+ ...options,
149
+ maxmem
150
+ }, (err, derived) => {
143
151
  if (err) reject(err);
144
152
  else resolve(derived);
145
153
  });
@@ -337,10 +345,72 @@ var UserService = class {
337
345
  store;
338
346
  config;
339
347
  hasher;
348
+ /** Moves on every store write through this service (start and end). */
349
+ writeEpoch = 0;
350
+ writeListeners = /* @__PURE__ */ new Set();
351
+ snapshots = /* @__PURE__ */ new WeakMap();
340
352
  constructor(store, config) {
341
353
  this.store = store;
342
354
  this.config = resolveConfig(config);
343
355
  this.hasher = new PasswordHasher(this.config.password);
356
+ this.store = observeWrites(store, {
357
+ start: () => {
358
+ this.writeEpoch++;
359
+ },
360
+ end: (id) => {
361
+ this.writeEpoch++;
362
+ let failure;
363
+ for (const listener of this.writeListeners) try {
364
+ listener(id);
365
+ } catch (error) {
366
+ failure ??= { error };
367
+ }
368
+ if (failure) throw failure.error;
369
+ }
370
+ });
371
+ }
372
+ /**
373
+ * Subscribe to user writes made through this service (or its store): the
374
+ * listener gets the written user's id once the write settles (`undefined`
375
+ * when it cannot be told). E.g. evict a cross-request cache:
376
+ * `users.onUserWrite((id) => arbacUserCache.invalidate(id))`.
377
+ *
378
+ * @returns unsubscribe
379
+ * @since 0.1.78
380
+ */
381
+ onUserWrite(listener) {
382
+ this.writeListeners.add(listener);
383
+ return () => {
384
+ this.writeListeners.delete(listener);
385
+ };
386
+ }
387
+ /**
388
+ * {@link getUser} memoized per `scope` — an object that lives as long as
389
+ * one unit of work (typically the current event context): repeated reads of
390
+ * the same user in that scope share one store read, until any write goes
391
+ * through this service (then the next read is fresh). Rejections are not
392
+ * kept. Writes made around this service are not seen — use `getUser` when
393
+ * that matters.
394
+ *
395
+ * @since 0.1.78
396
+ */
397
+ getUserSnapshot(id, scope) {
398
+ let perScope = this.snapshots.get(scope);
399
+ if (!perScope) {
400
+ perScope = /* @__PURE__ */ new Map();
401
+ this.snapshots.set(scope, perScope);
402
+ }
403
+ const hit = perScope.get(id);
404
+ if (hit && hit.epoch === this.writeEpoch) return hit.user;
405
+ const entry = {
406
+ epoch: this.writeEpoch,
407
+ user: this.getUser(id)
408
+ };
409
+ perScope.set(id, entry);
410
+ entry.user.catch(() => {
411
+ if (perScope.get(id) === entry) perScope.delete(id);
412
+ });
413
+ return entry.user;
344
414
  }
345
415
  /**
346
416
  * Creates a user with `account.active: false`. The invite workflow relies
@@ -964,6 +1034,40 @@ var UserService = class {
964
1034
  throw new require_federated_identity_store.UserAuthError(errorCode);
965
1035
  }
966
1036
  };
1037
+ /** The store methods that write (keyed by the user id, or `create`'s row). */
1038
+ const WRITE_METHODS = /* @__PURE__ */ new Set([
1039
+ "create",
1040
+ "update",
1041
+ "delete",
1042
+ "withCas"
1043
+ ]);
1044
+ /**
1045
+ * `store` with its write methods reporting to `hooks` (before the call and
1046
+ * once it settles, with the user id); everything else passes through, bound
1047
+ * to the store itself.
1048
+ */
1049
+ function observeWrites(store, hooks) {
1050
+ const bound = /* @__PURE__ */ new Map();
1051
+ return new Proxy(store, { get(target, prop) {
1052
+ const value = Reflect.get(target, prop, target);
1053
+ if (typeof value !== "function") return value;
1054
+ let fn = bound.get(prop);
1055
+ if (fn === void 0 || fn.__of !== value) {
1056
+ const method = value;
1057
+ fn = WRITE_METHODS.has(prop) ? Object.assign(async (...args) => {
1058
+ const id = typeof args[0] === "string" ? args[0] : args[0]?.id;
1059
+ hooks.start();
1060
+ try {
1061
+ return await method.apply(target, args);
1062
+ } finally {
1063
+ hooks.end(typeof id === "string" ? id : void 0);
1064
+ }
1065
+ }, { __of: value }) : Object.assign(method.bind(target), { __of: value });
1066
+ bound.set(prop, fn);
1067
+ }
1068
+ return fn;
1069
+ } });
1070
+ }
967
1071
  //#endregion
968
1072
  //#region src/store/memory.ts
969
1073
  var UserStoreMemory = class extends require_federated_identity_store.UserStore {
package/dist/index.d.cts CHANGED
@@ -85,7 +85,32 @@ declare class UserService<T extends object = object> {
85
85
  protected readonly store: UserStore<T>;
86
86
  protected readonly config: ResolvedConfig;
87
87
  protected readonly hasher: PasswordHasher;
88
+ /** Moves on every store write through this service (start and end). */
89
+ private writeEpoch;
90
+ private readonly writeListeners;
91
+ private readonly snapshots;
88
92
  constructor(store: UserStore<T>, config?: UserServiceConfig);
93
+ /**
94
+ * Subscribe to user writes made through this service (or its store): the
95
+ * listener gets the written user's id once the write settles (`undefined`
96
+ * when it cannot be told). E.g. evict a cross-request cache:
97
+ * `users.onUserWrite((id) => arbacUserCache.invalidate(id))`.
98
+ *
99
+ * @returns unsubscribe
100
+ * @since 0.1.78
101
+ */
102
+ onUserWrite(listener: (id: string | undefined) => void): () => void;
103
+ /**
104
+ * {@link getUser} memoized per `scope` — an object that lives as long as
105
+ * one unit of work (typically the current event context): repeated reads of
106
+ * the same user in that scope share one store read, until any write goes
107
+ * through this service (then the next read is fresh). Rejections are not
108
+ * kept. Writes made around this service are not seen — use `getUser` when
109
+ * that matters.
110
+ *
111
+ * @since 0.1.78
112
+ */
113
+ getUserSnapshot(id: string, scope: object): Promise<UserCredentials & T>;
89
114
  /**
90
115
  * Creates a user with `account.active: false`. The invite workflow relies
91
116
  * on this default (see `InviteWorkflow.acceptInvite` — pending invitees stay
package/dist/index.d.mts CHANGED
@@ -85,7 +85,32 @@ declare class UserService<T extends object = object> {
85
85
  protected readonly store: UserStore<T>;
86
86
  protected readonly config: ResolvedConfig;
87
87
  protected readonly hasher: PasswordHasher;
88
+ /** Moves on every store write through this service (start and end). */
89
+ private writeEpoch;
90
+ private readonly writeListeners;
91
+ private readonly snapshots;
88
92
  constructor(store: UserStore<T>, config?: UserServiceConfig);
93
+ /**
94
+ * Subscribe to user writes made through this service (or its store): the
95
+ * listener gets the written user's id once the write settles (`undefined`
96
+ * when it cannot be told). E.g. evict a cross-request cache:
97
+ * `users.onUserWrite((id) => arbacUserCache.invalidate(id))`.
98
+ *
99
+ * @returns unsubscribe
100
+ * @since 0.1.78
101
+ */
102
+ onUserWrite(listener: (id: string | undefined) => void): () => void;
103
+ /**
104
+ * {@link getUser} memoized per `scope` — an object that lives as long as
105
+ * one unit of work (typically the current event context): repeated reads of
106
+ * the same user in that scope share one store read, until any write goes
107
+ * through this service (then the next read is fresh). Rejections are not
108
+ * kept. Writes made around this service are not seen — use `getUser` when
109
+ * that matters.
110
+ *
111
+ * @since 0.1.78
112
+ */
113
+ getUserSnapshot(id: string, scope: object): Promise<UserCredentials & T>;
89
114
  /**
90
115
  * Creates a user with `account.active: false`. The invite workflow relies
91
116
  * on this default (see `InviteWorkflow.acceptInvite` — pending invitees stay
package/dist/index.mjs CHANGED
@@ -136,9 +136,17 @@ const DEFAULTS = {
136
136
  saltLength: 32
137
137
  };
138
138
  const PREFIX = "$scrypt$";
139
+ /** Node's default scrypt memory cap (32 MiB) — kept as the floor. */
140
+ const DEFAULT_MAXMEM = 32 * 1024 * 1024;
141
+ /** Upper bound for the memory a (possibly stored, untrusted) parameter set may ask for. */
142
+ const MAX_MAXMEM = 1024 * 1024 * 1024;
139
143
  function scryptAsync(password, salt, keyLength, options) {
144
+ const maxmem = Math.min(MAX_MAXMEM, Math.max(DEFAULT_MAXMEM, 256 * options.N * options.r + 1024 * 1024));
140
145
  return new Promise((resolve, reject) => {
141
- scrypt(password, salt, keyLength, options, (err, derived) => {
146
+ scrypt(password, salt, keyLength, {
147
+ ...options,
148
+ maxmem
149
+ }, (err, derived) => {
142
150
  if (err) reject(err);
143
151
  else resolve(derived);
144
152
  });
@@ -336,10 +344,72 @@ var UserService = class {
336
344
  store;
337
345
  config;
338
346
  hasher;
347
+ /** Moves on every store write through this service (start and end). */
348
+ writeEpoch = 0;
349
+ writeListeners = /* @__PURE__ */ new Set();
350
+ snapshots = /* @__PURE__ */ new WeakMap();
339
351
  constructor(store, config) {
340
352
  this.store = store;
341
353
  this.config = resolveConfig(config);
342
354
  this.hasher = new PasswordHasher(this.config.password);
355
+ this.store = observeWrites(store, {
356
+ start: () => {
357
+ this.writeEpoch++;
358
+ },
359
+ end: (id) => {
360
+ this.writeEpoch++;
361
+ let failure;
362
+ for (const listener of this.writeListeners) try {
363
+ listener(id);
364
+ } catch (error) {
365
+ failure ??= { error };
366
+ }
367
+ if (failure) throw failure.error;
368
+ }
369
+ });
370
+ }
371
+ /**
372
+ * Subscribe to user writes made through this service (or its store): the
373
+ * listener gets the written user's id once the write settles (`undefined`
374
+ * when it cannot be told). E.g. evict a cross-request cache:
375
+ * `users.onUserWrite((id) => arbacUserCache.invalidate(id))`.
376
+ *
377
+ * @returns unsubscribe
378
+ * @since 0.1.78
379
+ */
380
+ onUserWrite(listener) {
381
+ this.writeListeners.add(listener);
382
+ return () => {
383
+ this.writeListeners.delete(listener);
384
+ };
385
+ }
386
+ /**
387
+ * {@link getUser} memoized per `scope` — an object that lives as long as
388
+ * one unit of work (typically the current event context): repeated reads of
389
+ * the same user in that scope share one store read, until any write goes
390
+ * through this service (then the next read is fresh). Rejections are not
391
+ * kept. Writes made around this service are not seen — use `getUser` when
392
+ * that matters.
393
+ *
394
+ * @since 0.1.78
395
+ */
396
+ getUserSnapshot(id, scope) {
397
+ let perScope = this.snapshots.get(scope);
398
+ if (!perScope) {
399
+ perScope = /* @__PURE__ */ new Map();
400
+ this.snapshots.set(scope, perScope);
401
+ }
402
+ const hit = perScope.get(id);
403
+ if (hit && hit.epoch === this.writeEpoch) return hit.user;
404
+ const entry = {
405
+ epoch: this.writeEpoch,
406
+ user: this.getUser(id)
407
+ };
408
+ perScope.set(id, entry);
409
+ entry.user.catch(() => {
410
+ if (perScope.get(id) === entry) perScope.delete(id);
411
+ });
412
+ return entry.user;
343
413
  }
344
414
  /**
345
415
  * Creates a user with `account.active: false`. The invite workflow relies
@@ -963,6 +1033,40 @@ var UserService = class {
963
1033
  throw new UserAuthError(errorCode);
964
1034
  }
965
1035
  };
1036
+ /** The store methods that write (keyed by the user id, or `create`'s row). */
1037
+ const WRITE_METHODS = /* @__PURE__ */ new Set([
1038
+ "create",
1039
+ "update",
1040
+ "delete",
1041
+ "withCas"
1042
+ ]);
1043
+ /**
1044
+ * `store` with its write methods reporting to `hooks` (before the call and
1045
+ * once it settles, with the user id); everything else passes through, bound
1046
+ * to the store itself.
1047
+ */
1048
+ function observeWrites(store, hooks) {
1049
+ const bound = /* @__PURE__ */ new Map();
1050
+ return new Proxy(store, { get(target, prop) {
1051
+ const value = Reflect.get(target, prop, target);
1052
+ if (typeof value !== "function") return value;
1053
+ let fn = bound.get(prop);
1054
+ if (fn === void 0 || fn.__of !== value) {
1055
+ const method = value;
1056
+ fn = WRITE_METHODS.has(prop) ? Object.assign(async (...args) => {
1057
+ const id = typeof args[0] === "string" ? args[0] : args[0]?.id;
1058
+ hooks.start();
1059
+ try {
1060
+ return await method.apply(target, args);
1061
+ } finally {
1062
+ hooks.end(typeof id === "string" ? id : void 0);
1063
+ }
1064
+ }, { __of: value }) : Object.assign(method.bind(target), { __of: value });
1065
+ bound.set(prop, fn);
1066
+ }
1067
+ return fn;
1068
+ } });
1069
+ }
966
1070
  //#endregion
967
1071
  //#region src/store/memory.ts
968
1072
  var UserStoreMemory = class extends UserStore {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aooth/user",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "User credential primitives for aoothjs",
5
5
  "keywords": [
6
6
  "aoothjs",
@@ -60,18 +60,18 @@
60
60
  "access": "public"
61
61
  },
62
62
  "devDependencies": {
63
- "@atscript/core": "^0.1.101",
64
- "@atscript/db": "^0.1.150",
65
- "@atscript/db-sql-tools": "^0.1.150",
66
- "@atscript/db-sqlite": "^0.1.150",
67
- "@atscript/typescript": "^0.1.101",
63
+ "@atscript/core": "^0.1.103",
64
+ "@atscript/db": "^0.1.153",
65
+ "@atscript/db-sql-tools": "^0.1.153",
66
+ "@atscript/db-sqlite": "^0.1.153",
67
+ "@atscript/typescript": "^0.1.103",
68
68
  "@types/better-sqlite3": "^7.6.13",
69
- "@uniqu/core": "^0.1.13",
69
+ "@uniqu/core": "^0.1.15",
70
70
  "better-sqlite3": "^12.6.2",
71
- "unplugin-atscript": "^0.1.101"
71
+ "unplugin-atscript": "^0.1.103"
72
72
  },
73
73
  "peerDependencies": {
74
- "@atscript/db": "^0.1.150"
74
+ "@atscript/db": "^0.1.153"
75
75
  },
76
76
  "peerDependenciesMeta": {
77
77
  "@atscript/db": {