@aooth/auth 0.1.19 → 0.1.21

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.
@@ -233,13 +233,15 @@ function rowToDynamicClient(row) {
233
233
  */
234
234
  var CredentialStoreAtscriptDb = class {
235
235
  table;
236
+ metadataField;
236
237
  constructor(opts) {
237
238
  this.table = opts.table;
239
+ this.metadataField = opts.metadataField;
238
240
  }
239
241
  async persist(state, ttl) {
240
242
  const token = (0, node_crypto.randomUUID)();
241
243
  const expiresAt = typeof ttl === "number" ? Date.now() + ttl : state.expiresAt;
242
- await this.table.insertOne(stateToRow(state, token, expiresAt));
244
+ await this.table.insertOne(stateToRow(state, token, expiresAt, this.metadataField));
243
245
  return token;
244
246
  }
245
247
  async retrieve(token) {
@@ -249,7 +251,7 @@ var CredentialStoreAtscriptDb = class {
249
251
  await this.table.deleteOne(token).catch(() => {});
250
252
  return null;
251
253
  }
252
- return rowToState(row);
254
+ return rowToState(row, this.metadataField);
253
255
  }
254
256
  async consume(token) {
255
257
  const state = await this.retrieve(token);
@@ -263,7 +265,7 @@ var CredentialStoreAtscriptDb = class {
263
265
  await this.revoke(token);
264
266
  return token;
265
267
  }
266
- await this.table.replaceOne(stateToRow(state, token, state.expiresAt));
268
+ await this.table.replaceOne(stateToRow(state, token, state.expiresAt, this.metadataField));
267
269
  return token;
268
270
  }
269
271
  async revoke(token) {
@@ -292,7 +294,7 @@ var CredentialStoreAtscriptDb = class {
292
294
  continue;
293
295
  }
294
296
  out.push({
295
- ...rowToState(row),
297
+ ...rowToState(row, this.metadataField),
296
298
  token: row.token
297
299
  });
298
300
  }
@@ -305,30 +307,45 @@ var CredentialStoreAtscriptDb = class {
305
307
  * (so an envelope field wins a name clash), then the fixed envelope fields and
306
308
  * the resolved `token` + `expiresAt`. Shared by `persist` and `update`, which
307
309
  * differ only in the `expiresAt` they resolve.
310
+ *
311
+ * The envelope's `metadata` is written to the consumer's `metadataField`
312
+ * column (a dynamic key the static row type can't carry) — only when the
313
+ * field is configured; otherwise metadata is silently not persisted.
308
314
  */
309
- function stateToRow(state, token, expiresAt) {
310
- return {
315
+ function stateToRow(state, token, expiresAt, metadataField) {
316
+ const row = {
311
317
  ...require_payload.credentialPayloadOf(state),
312
318
  token,
313
319
  userId: state.userId,
314
320
  issuedAt: state.issuedAt,
315
321
  expiresAt,
316
322
  kind: state.kind,
317
- metadata: state.metadata,
318
323
  parentCredentialId: state.parentCredentialId,
319
324
  rotatedAt: state.rotatedAt,
320
325
  sessionId: state.sessionId,
321
326
  lastSeenAt: state.lastSeenAt
322
327
  };
328
+ if (metadataField !== void 0 && state.metadata !== void 0) row[metadataField] = state.metadata;
329
+ return row;
323
330
  }
324
- function rowToState(row) {
331
+ /**
332
+ * Inverse of {@link stateToRow}. The consumer's `metadataField` column maps
333
+ * back onto the envelope's `metadata` — and is excluded from the extracted
334
+ * payload (it is envelope data riding under a consumer-chosen name, not a
335
+ * typed payload field).
336
+ */
337
+ function rowToState(row, metadataField) {
325
338
  const state = {
326
339
  ...require_payload.credentialPayloadOf(row),
327
340
  userId: row.userId,
328
341
  issuedAt: row.issuedAt,
329
342
  expiresAt: row.expiresAt
330
343
  };
331
- if (row.metadata !== void 0) state.metadata = row.metadata;
344
+ if (metadataField !== void 0) {
345
+ delete state[metadataField];
346
+ const metadata = row[metadataField];
347
+ if (metadata !== void 0) state.metadata = metadata;
348
+ }
332
349
  if (row.kind === "access" || row.kind === "refresh") state.kind = row.kind;
333
350
  if (row.parentCredentialId !== void 0) state.parentCredentialId = row.parentCredentialId;
334
351
  if (row.rotatedAt !== void 0) state.rotatedAt = row.rotatedAt;
@@ -210,6 +210,11 @@ declare class DynamicClientStoreAtscriptDb extends DynamicClientStore {
210
210
  * The consumer's typed payload `TPayload` (the root fields they add to their
211
211
  * `extends AoothAuthCredential` model) is intersected flat — those become real
212
212
  * typed columns, replacing the dropped free-form `claims` blob.
213
+ *
214
+ * There is NO `metadata` envelope column: credential metadata is
215
+ * consumer-declared (a fully-typed `@db.json` field on the extending model,
216
+ * marked `@aooth.auth.metadata`) and mapped dynamically through the
217
+ * `metadataField` option below.
213
218
  */
214
219
  type AuthCredentialRow<TPayload extends object = object> = {
215
220
  token: string;
@@ -217,12 +222,6 @@ type AuthCredentialRow<TPayload extends object = object> = {
217
222
  issuedAt: number;
218
223
  expiresAt: number;
219
224
  kind?: string;
220
- metadata?: {
221
- ip?: string;
222
- userAgent?: string;
223
- fingerprint?: string;
224
- label?: string;
225
- };
226
225
  parentCredentialId?: string;
227
226
  rotatedAt?: number;
228
227
  sessionId?: string;
@@ -258,6 +257,15 @@ interface AuthCredentialTable<TPayload extends object = object> {
258
257
  }
259
258
  interface CredentialStoreAtscriptDbOptions<TPayload extends object> {
260
259
  table: AuthCredentialTable<TPayload>;
260
+ /**
261
+ * Name of the consumer's `@aooth.auth.metadata`-annotated column — the
262
+ * fully-typed `@db.json` field declared on their `extends AoothAuthCredential`
263
+ * model that persists the envelope's `metadata`. Resolved at boot by
264
+ * `getAoothCredentialMetadataSpec` (`@aooth/arbac-moost/atscript`) and
265
+ * threaded here as plain config — same pattern as `UserStore.handleFields`.
266
+ * Absent → metadata is not persisted/read by this store.
267
+ */
268
+ metadataField?: string;
261
269
  }
262
270
  /**
263
271
  * atscript-db-backed `CredentialStore`.
@@ -274,6 +282,7 @@ interface CredentialStoreAtscriptDbOptions<TPayload extends object> {
274
282
  */
275
283
  declare class CredentialStoreAtscriptDb<TPayload extends object = object> implements CredentialStore<TPayload> {
276
284
  private readonly table;
285
+ private readonly metadataField;
277
286
  constructor(opts: CredentialStoreAtscriptDbOptions<TPayload>);
278
287
  persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
279
288
  retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
@@ -210,6 +210,11 @@ declare class DynamicClientStoreAtscriptDb extends DynamicClientStore {
210
210
  * The consumer's typed payload `TPayload` (the root fields they add to their
211
211
  * `extends AoothAuthCredential` model) is intersected flat — those become real
212
212
  * typed columns, replacing the dropped free-form `claims` blob.
213
+ *
214
+ * There is NO `metadata` envelope column: credential metadata is
215
+ * consumer-declared (a fully-typed `@db.json` field on the extending model,
216
+ * marked `@aooth.auth.metadata`) and mapped dynamically through the
217
+ * `metadataField` option below.
213
218
  */
214
219
  type AuthCredentialRow<TPayload extends object = object> = {
215
220
  token: string;
@@ -217,12 +222,6 @@ type AuthCredentialRow<TPayload extends object = object> = {
217
222
  issuedAt: number;
218
223
  expiresAt: number;
219
224
  kind?: string;
220
- metadata?: {
221
- ip?: string;
222
- userAgent?: string;
223
- fingerprint?: string;
224
- label?: string;
225
- };
226
225
  parentCredentialId?: string;
227
226
  rotatedAt?: number;
228
227
  sessionId?: string;
@@ -258,6 +257,15 @@ interface AuthCredentialTable<TPayload extends object = object> {
258
257
  }
259
258
  interface CredentialStoreAtscriptDbOptions<TPayload extends object> {
260
259
  table: AuthCredentialTable<TPayload>;
260
+ /**
261
+ * Name of the consumer's `@aooth.auth.metadata`-annotated column — the
262
+ * fully-typed `@db.json` field declared on their `extends AoothAuthCredential`
263
+ * model that persists the envelope's `metadata`. Resolved at boot by
264
+ * `getAoothCredentialMetadataSpec` (`@aooth/arbac-moost/atscript`) and
265
+ * threaded here as plain config — same pattern as `UserStore.handleFields`.
266
+ * Absent → metadata is not persisted/read by this store.
267
+ */
268
+ metadataField?: string;
261
269
  }
262
270
  /**
263
271
  * atscript-db-backed `CredentialStore`.
@@ -274,6 +282,7 @@ interface CredentialStoreAtscriptDbOptions<TPayload extends object> {
274
282
  */
275
283
  declare class CredentialStoreAtscriptDb<TPayload extends object = object> implements CredentialStore<TPayload> {
276
284
  private readonly table;
285
+ private readonly metadataField;
277
286
  constructor(opts: CredentialStoreAtscriptDbOptions<TPayload>);
278
287
  persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
279
288
  retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
@@ -232,13 +232,15 @@ function rowToDynamicClient(row) {
232
232
  */
233
233
  var CredentialStoreAtscriptDb = class {
234
234
  table;
235
+ metadataField;
235
236
  constructor(opts) {
236
237
  this.table = opts.table;
238
+ this.metadataField = opts.metadataField;
237
239
  }
238
240
  async persist(state, ttl) {
239
241
  const token = randomUUID();
240
242
  const expiresAt = typeof ttl === "number" ? Date.now() + ttl : state.expiresAt;
241
- await this.table.insertOne(stateToRow(state, token, expiresAt));
243
+ await this.table.insertOne(stateToRow(state, token, expiresAt, this.metadataField));
242
244
  return token;
243
245
  }
244
246
  async retrieve(token) {
@@ -248,7 +250,7 @@ var CredentialStoreAtscriptDb = class {
248
250
  await this.table.deleteOne(token).catch(() => {});
249
251
  return null;
250
252
  }
251
- return rowToState(row);
253
+ return rowToState(row, this.metadataField);
252
254
  }
253
255
  async consume(token) {
254
256
  const state = await this.retrieve(token);
@@ -262,7 +264,7 @@ var CredentialStoreAtscriptDb = class {
262
264
  await this.revoke(token);
263
265
  return token;
264
266
  }
265
- await this.table.replaceOne(stateToRow(state, token, state.expiresAt));
267
+ await this.table.replaceOne(stateToRow(state, token, state.expiresAt, this.metadataField));
266
268
  return token;
267
269
  }
268
270
  async revoke(token) {
@@ -291,7 +293,7 @@ var CredentialStoreAtscriptDb = class {
291
293
  continue;
292
294
  }
293
295
  out.push({
294
- ...rowToState(row),
296
+ ...rowToState(row, this.metadataField),
295
297
  token: row.token
296
298
  });
297
299
  }
@@ -304,30 +306,45 @@ var CredentialStoreAtscriptDb = class {
304
306
  * (so an envelope field wins a name clash), then the fixed envelope fields and
305
307
  * the resolved `token` + `expiresAt`. Shared by `persist` and `update`, which
306
308
  * differ only in the `expiresAt` they resolve.
309
+ *
310
+ * The envelope's `metadata` is written to the consumer's `metadataField`
311
+ * column (a dynamic key the static row type can't carry) — only when the
312
+ * field is configured; otherwise metadata is silently not persisted.
307
313
  */
308
- function stateToRow(state, token, expiresAt) {
309
- return {
314
+ function stateToRow(state, token, expiresAt, metadataField) {
315
+ const row = {
310
316
  ...credentialPayloadOf(state),
311
317
  token,
312
318
  userId: state.userId,
313
319
  issuedAt: state.issuedAt,
314
320
  expiresAt,
315
321
  kind: state.kind,
316
- metadata: state.metadata,
317
322
  parentCredentialId: state.parentCredentialId,
318
323
  rotatedAt: state.rotatedAt,
319
324
  sessionId: state.sessionId,
320
325
  lastSeenAt: state.lastSeenAt
321
326
  };
327
+ if (metadataField !== void 0 && state.metadata !== void 0) row[metadataField] = state.metadata;
328
+ return row;
322
329
  }
323
- function rowToState(row) {
330
+ /**
331
+ * Inverse of {@link stateToRow}. The consumer's `metadataField` column maps
332
+ * back onto the envelope's `metadata` — and is excluded from the extracted
333
+ * payload (it is envelope data riding under a consumer-chosen name, not a
334
+ * typed payload field).
335
+ */
336
+ function rowToState(row, metadataField) {
324
337
  const state = {
325
338
  ...credentialPayloadOf(row),
326
339
  userId: row.userId,
327
340
  issuedAt: row.issuedAt,
328
341
  expiresAt: row.expiresAt
329
342
  };
330
- if (row.metadata !== void 0) state.metadata = row.metadata;
343
+ if (metadataField !== void 0) {
344
+ delete state[metadataField];
345
+ const metadata = row[metadataField];
346
+ if (metadata !== void 0) state.metadata = metadata;
347
+ }
331
348
  if (row.kind === "access" || row.kind === "refresh") state.kind = row.kind;
332
349
  if (row.parentCredentialId !== void 0) state.parentCredentialId = row.parentCredentialId;
333
350
  if (row.rotatedAt !== void 0) state.rotatedAt = row.rotatedAt;
package/dist/index.d.cts CHANGED
@@ -420,9 +420,11 @@ declare class AuthCredential<TPayload extends object = object> {
420
420
  *
421
421
  * `mfa.code` is reserved for v2 — v1 is TOTP only. `login.pincode`,
422
422
  * `recovery.pincode`, `invite.pincode` and `notifyNewDevice` are added with
423
- * BIG 3.1 (login workflow re-implementation).
423
+ * BIG 3.1 (login workflow re-implementation). `securityAlert` is the
424
+ * consumer-triggered security notice (e.g. impossible-travel from a
425
+ * `resolveRiskStepUp` override) — never auto-sent by the framework.
424
426
  */
425
- type AuthEmailKind = "recovery.magicLink" | "invite.magicLink" | "mfa.code" | "login.pincode" | "recovery.pincode" | "invite.pincode" | "notifyNewDevice";
427
+ type AuthEmailKind = "recovery.magicLink" | "invite.magicLink" | "mfa.code" | "login.pincode" | "recovery.pincode" | "invite.pincode" | "notifyNewDevice" | "securityAlert";
426
428
  /**
427
429
  * Structured event passed to `EmailSender.send()` from inside the auth
428
430
  * workflows. Flat and serialisable so consumers can route it to any
package/dist/index.d.mts CHANGED
@@ -420,9 +420,11 @@ declare class AuthCredential<TPayload extends object = object> {
420
420
  *
421
421
  * `mfa.code` is reserved for v2 — v1 is TOTP only. `login.pincode`,
422
422
  * `recovery.pincode`, `invite.pincode` and `notifyNewDevice` are added with
423
- * BIG 3.1 (login workflow re-implementation).
423
+ * BIG 3.1 (login workflow re-implementation). `securityAlert` is the
424
+ * consumer-triggered security notice (e.g. impossible-travel from a
425
+ * `resolveRiskStepUp` override) — never auto-sent by the framework.
424
426
  */
425
- type AuthEmailKind = "recovery.magicLink" | "invite.magicLink" | "mfa.code" | "login.pincode" | "recovery.pincode" | "invite.pincode" | "notifyNewDevice";
427
+ type AuthEmailKind = "recovery.magicLink" | "invite.magicLink" | "mfa.code" | "login.pincode" | "recovery.pincode" | "invite.pincode" | "notifyNewDevice" | "securityAlert";
426
428
  /**
427
429
  * Structured event passed to `EmailSender.send()` from inside the auth
428
430
  * workflows. Flat and serialisable so consumers can route it to any
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aooth/auth",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Auth method layer for aoothjs (sessions, tokens, password reset, MFA primitives)",
5
5
  "keywords": [
6
6
  "aoothjs",
@@ -104,17 +104,17 @@
104
104
  },
105
105
  "dependencies": {
106
106
  "jose": "^6.2.3",
107
- "@aooth/user": "0.1.19"
107
+ "@aooth/user": "0.1.21"
108
108
  },
109
109
  "devDependencies": {
110
- "@atscript/core": "^0.1.75",
111
- "@atscript/db": "^0.1.104",
112
- "@atscript/db-sql-tools": "^0.1.104",
113
- "@atscript/db-sqlite": "^0.1.104",
114
- "@atscript/typescript": "^0.1.75",
110
+ "@atscript/core": "^0.1.76",
111
+ "@atscript/db": "^0.1.106",
112
+ "@atscript/db-sql-tools": "^0.1.106",
113
+ "@atscript/db-sqlite": "^0.1.106",
114
+ "@atscript/typescript": "^0.1.76",
115
115
  "@types/better-sqlite3": "^7.6.13",
116
116
  "better-sqlite3": "^12.6.2",
117
- "unplugin-atscript": "^0.1.75"
117
+ "unplugin-atscript": "^0.1.76"
118
118
  },
119
119
  "peerDependencies": {
120
120
  "@atscript/db": ">=0.1.79"
@@ -1,3 +1,36 @@
1
+ /**
2
+ * The framework-written credential-metadata envelope keys — the `.as` twin of
3
+ * `CredentialMetadata` in `@aooth/auth` (keep the two in sync). The bundled
4
+ * row model ships NO metadata column; a consumer who wants metadata persisted
5
+ * declares their own `@aooth.auth.metadata @db.json` field and builds its shape
6
+ * by INTERSECTING this type with their extension keys:
7
+ *
8
+ * import { AoothAuthCredential, AoothCredentialMetadataBase } from '@aooth/auth/atscript-db/model'
9
+ *
10
+ * @aooth.auth.metadata
11
+ * @db.json
12
+ * metadata?: AoothCredentialMetadataBase & { geoLat?: number, geoLon?: number }
13
+ *
14
+ * That keeps one source of truth for the framework keys: when a future aooth
15
+ * release adds an envelope key here, consumer schemas pick it up on upgrade
16
+ * instead of silently rejecting it from their hand-mirrored closed shape.
17
+ */
18
+ export type AoothCredentialMetadataBase = {
19
+ /** Client IP captured at issue time (default `resolveIssueMetadata`). */
20
+ ip?: string
21
+ /** Raw User-Agent captured at issue time. */
22
+ userAgent?: string
23
+ /** Consumer-supplied device fingerprint (`IssueOptions.fingerprint`). */
24
+ fingerprint?: string
25
+ /** Human-readable session label. */
26
+ label?: string
27
+ /**
28
+ * Semantic credential kind (e.g. "cli-session" / "pat") — distinct from
29
+ * the internal access/refresh `kind` column on the row.
30
+ */
31
+ credentialKind?: string
32
+ }
33
+
1
34
  @db.table 'aooth_credentials'
2
35
  @db.depth.limit 0
3
36
  export interface AoothAuthCredential {
@@ -23,20 +56,13 @@ export interface AoothAuthCredential {
23
56
  */
24
57
  kind?: string
25
58
 
26
- /**
27
- * Display metadata: ip, userAgent, fingerprint, label, plus the semantic
28
- * credential kind (`cli-session` / `pat` / ) folded in by `issue()`. The
29
- * `@db.json` column is schema-validated, so every field `CredentialMetadata`
30
- * may carry must be declared here.
31
- */
32
- @db.json
33
- metadata?: {
34
- ip?: string
35
- userAgent?: string
36
- fingerprint?: string
37
- label?: string
38
- credentialKind?: string
39
- }
59
+ // NO metadata column here — credential metadata is CONSUMER-DECLARED.
60
+ // Declare a fully-typed `@db.json` field on your extending model (the
61
+ // runtime/validation twin of your `CredentialMetadata` declaration merge)
62
+ // and mark it `@aooth.auth.metadata`; without one, the atscript-db store
63
+ // persists no metadata (memory/JWT stores are unaffected). Build the shape
64
+ // as `AoothCredentialMetadataBase & { ...your keys }` (exported above) so
65
+ // the framework-written keys stay single-sourced.
40
66
 
41
67
  /** Set by refresh-token rotation. */
42
68
  parentCredentialId?: string
@@ -9,9 +9,31 @@
9
9
 
10
10
  import type { TAtscriptTypeObject, TAtscriptTypeComplex, TAtscriptTypeFinal, TAtscriptTypeArray, TAtscriptAnnotatedType, TMetadataMap, Validator, TValidatorOptions } from "@atscript/typescript/utils"
11
11
 
12
+ /**
13
+ * Atscript type **AoothCredentialMetadataBase**
14
+ * @see {@link ./auth-credential.as:18:13}
15
+ */
16
+ export type AoothCredentialMetadataBase = {
17
+ ip?: string
18
+ userAgent?: string
19
+ fingerprint?: string
20
+ label?: string
21
+ credentialKind?: string
22
+ }
23
+ declare namespace AoothCredentialMetadataBase {
24
+ const __is_atscript_annotated_type: true
25
+ const type: TAtscriptTypeObject<keyof AoothCredentialMetadataBase, AoothCredentialMetadataBase>
26
+ const metadata: TMetadataMap<AtscriptMetadata>
27
+ const validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof AoothCredentialMetadataBase>
28
+ /** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */
29
+ const toJsonSchema: () => any
30
+ /** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */
31
+ const toExampleData: (() => any) | undefined
32
+ }
33
+
12
34
  /**
13
35
  * Atscript interface **AoothAuthCredential**
14
- * @see {@link ./auth-credential.as:3:18}
36
+ * @see {@link ./auth-credential.as:36:18}
15
37
  */
16
38
  export declare class AoothAuthCredential {
17
39
  token: string
@@ -19,13 +41,6 @@ export declare class AoothAuthCredential {
19
41
  issuedAt: number /* timestamp */
20
42
  expiresAt: number /* timestamp */
21
43
  kind?: string
22
- metadata?: {
23
- ip?: string
24
- userAgent?: string
25
- fingerprint?: string
26
- label?: string
27
- credentialKind?: string
28
- }
29
44
  parentCredentialId?: string
30
45
  rotatedAt?: number /* timestamp */
31
46
  sessionId?: string
@@ -44,7 +59,6 @@ export declare class AoothAuthCredential {
44
59
  "issuedAt": number /* timestamp */
45
60
  "expiresAt": number /* timestamp */
46
61
  "kind"?: string
47
- "metadata"?: string
48
62
  "parentCredentialId"?: string
49
63
  "rotatedAt"?: number /* timestamp */
50
64
  "sessionId"?: string
@@ -56,7 +70,6 @@ export declare class AoothAuthCredential {
56
70
  "issuedAt": number /* timestamp */
57
71
  "expiresAt": number /* timestamp */
58
72
  "kind"?: string
59
- "metadata"?: string
60
73
  "parentCredentialId"?: string
61
74
  "rotatedAt"?: number /* timestamp */
62
75
  "sessionId"?: string