@nxgt/janus-telemetry 0.6.0 → 0.7.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus) on
4
4
  [`@nxgt/telemetry`](https://www.npmjs.com/package/@nxgt/telemetry): a span per
5
5
  flow and per permission check, and the security events an audit reads — who
6
- signed up, in and out, who was sent a sign-in code, why a sign-in was
6
+ signed up, in and out, who was sent a sign-in code or link, why a sign-in was
7
7
  refused, who was asked for a second factor and who enrolled, activated or
8
8
  disabled one, which users were deleted
9
9
  or deactivated, whose password changed, and who was granted what. **Never a
@@ -53,7 +53,7 @@ current span. Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`.
53
53
 
54
54
  | Span | Attributes |
55
55
  | --- | --- |
56
- | `janus.<flow>`, `janus.<type>.<flow>` — `janus.secondFactor.confirm` and `janus.signInCode.confirm` included | `janus.user.type`; `user.id` once the answer names a user; `janus.session.renewed` for `authenticate`; `janus.signIn.status` for `signIn` and `signInCode.confirm` — `signedIn`, or `secondFactor` when it answered a challenge; for `stepUp.confirm`, the `user.id` of the session it stamped, never the session's id |
56
+ | `janus.<flow>`, `janus.<type>.<flow>` — `janus.secondFactor.confirm`, `janus.signInCode.confirm` and `janus.magicLink.confirm` included | `janus.user.type`; `user.id` once the answer names a user; `janus.session.renewed` for `authenticate`; `janus.signIn.status` for `signIn`, `signInCode.confirm` and `magicLink.confirm` — `signedIn`, or `secondFactor` when it answered a challenge; for `stepUp.confirm`, the `user.id` of the session it stamped, never the session's id |
57
57
  | `janus.can` | `janus.subject.type`, `janus.subject.id`, `janus.permission`, `janus.object.type`, `janus.object.id`, and the answer, `janus.allowed` |
58
58
  | `janus.list` | the subject, `janus.permission`, `janus.object.type`, and `janus.page.items`, how many it found |
59
59
  | `janus.grant`, `janus.revoke` | the object, `janus.relation`, and the subject — `janus.subject.relation` for a subject set, read as `permissions()` reads it: a user with a field named `relation` is that user, and a set on a user type is one only when `setOf()` made it |
@@ -74,9 +74,10 @@ when the flow knows them — `janus.signOut` carries neither:
74
74
  | --- | --- | --- |
75
75
  | `janus.signUp` | info | a user signed up |
76
76
  | `janus.signInCode.sent` | info | `signInCode.request` issued a code, with the `user.id` it is for. A request that answered `null` writes nothing |
77
- | `janus.signIn` | info | a user signed in — by `signIn`; by `signInCode.confirm`, which adds `janus.signIn.code: true`; by `secondFactor.confirm`, which adds `janus.signIn.secondFactor: true`; or by `secondFactor.recover`, which adds `janus.signIn.recoveryCode: true` and `janus.secondFactor.recoveryCodesLeft` — a count, never a code |
78
- | `janus.signIn.secondFactor` | info | the password, or an e-mailed code, was right and a second factor's code was asked for: `signIn` or `signInCode.confirm` answered a challenge. `janus.user.type` and the `user.id` the challenge answer carries — never the login typed |
79
- | `janus.signIn.refused` | **warn** | a sign-in was refused, with `janus.refusal` — `CREDENTIALS_INVALID` with `janus.refusal.reason` (`unknownLogin`, `noPassword`, `wrongPassword`), `USER_INACTIVE` with the `user.id` of the deactivated user, or a refused `secondFactor.confirm` or `signInCode.confirm`: `CODE_INVALID` with `user.id` and `janus.secondFactor.attemptsLeft` — the same attribute for both — a `TOKEN_*` code (`TOKEN_STALE` with `user.id`), `SECOND_FACTOR_NOT_ENROLLED`, or `VERSION_CONFLICT` for a write that raced. Every refusal of `signInCode.confirm` adds `janus.signIn.code: true`, and every refusal of `secondFactor.recover` adds `janus.signIn.recoveryCode: true` |
77
+ | `janus.magicLink.sent` | info | `magicLink.request` issued a sign-in link, with the `user.id` it is for — never its token. A request that answered `null` writes nothing |
78
+ | `janus.signIn` | info | a user signed in — by `signIn`; by `signInCode.confirm`, which adds `janus.signIn.code: true`; by `magicLink.confirm`, which adds `janus.signIn.magicLink: true`; by `secondFactor.confirm`, which adds `janus.signIn.secondFactor: true`; or by `secondFactor.recover`, which adds `janus.signIn.recoveryCode: true` and `janus.secondFactor.recoveryCodesLeft` — a count, never a code |
79
+ | `janus.signIn.secondFactor` | info | the password, an e-mailed code or a sign-in link was right and a second factor's code was asked for: `signIn`, `signInCode.confirm` or `magicLink.confirm` answered a challenge. `janus.user.type` and the `user.id` the challenge answer carries — never the login typed |
80
+ | `janus.signIn.refused` | **warn** | a sign-in was refused, with `janus.refusal` — `CREDENTIALS_INVALID` with `janus.refusal.reason` (`unknownLogin`, `noPassword`, `wrongPassword`), `USER_INACTIVE` with the `user.id` of the deactivated user, or a refused `secondFactor.confirm` or `signInCode.confirm`: `CODE_INVALID` with `user.id` and `janus.secondFactor.attemptsLeft` — the same attribute for both — a `TOKEN_*` code (`TOKEN_STALE` with `user.id`), `SECOND_FACTOR_NOT_ENROLLED`, or `VERSION_CONFLICT` for a write that raced. Every refusal of `signInCode.confirm` adds `janus.signIn.code: true`, every refusal of `magicLink.confirm` — a `TOKEN_*` code, `USER_INACTIVE` or `VERSION_CONFLICT` — adds `janus.signIn.magicLink: true`, and every refusal of `secondFactor.recover` adds `janus.signIn.recoveryCode: true` |
80
81
  | `janus.signIn.throttled` | **warn** | `signIn` refused a login past its attempts in the window — `@nxgt/janus`'s throttle, the right password included — with `janus.refusal: CREDENTIALS_INVALID`, `janus.refusal.reason: throttled`, `janus.user.type` and `janus.signIn.retryAfter`, the seconds until the next window. Never the login, never a `user.id`: an unknown login is throttled alike. Written instead of `janus.signIn.refused`, since no password was compared |
81
82
  | `janus.secondFactor.enrolled`, `janus.secondFactor.activated`, `janus.secondFactor.disabled` | info | `enroll`, `activate` and `disable`, with the `user.id` they were called for |
82
83
  | `janus.secondFactor.recoveryCodesRegenerated` | info | `regenerateRecoveryCodes`, with the `user.id` it was called for — never a code |
@@ -108,8 +109,8 @@ when the flow knows them — `janus.signOut` carries neither:
108
109
  application's, from the request. Only a refusal after the password was right, or against a
109
110
  challenge, carries `user.id`: `USER_INACTIVE`, a `CODE_INVALID`,
110
111
  `TOKEN_STALE` or `SECOND_FACTOR_NOT_ENROLLED`. Nor does
111
- `janus.signInCode.sent` name the address: a request for nobody writes
112
- nothing at all.
112
+ `janus.signInCode.sent` or `janus.magicLink.sent` name the address: a
113
+ request for nobody writes nothing at all.
113
114
  - **A wrong code is a warning, not a failure.** `CODE_INVALID` leaves the
114
115
  `janus.secondFactor.confirm`, `janus.secondFactor.recover` or
115
116
  `janus.signInCode.confirm` span `ok`, and
@@ -119,6 +120,11 @@ when the flow knows them — `janus.signOut` carries neither:
119
120
  carries `janus.signIn.code: true` — `CODE_INVALID`, a `TOKEN_*` code
120
121
  including `TOKEN_STALE`, `USER_INACTIVE` and `VERSION_CONFLICT` alike — so
121
122
  a filter on the mark sees the whole e-mailed-code flow.
123
+ - **A spent sign-in link is a warning too.** `magicLink.confirm`'s refusals
124
+ leave its span `ok` and write `janus.signIn.refused` with
125
+ `janus.signIn.magicLink: true`. Many `TOKEN_SPENT` on links never
126
+ confirmed twice by your users is the sign of a mail scanner opening them:
127
+ confirm from a `POST`, never from the link's `GET`.
122
128
  - **One copy of `@nxgt/janus`.** A refusal is told from a failure by
123
129
  `instanceof JanusError`: with a second copy installed, every wrong password
124
130
  fails its span.
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/flows/events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AA0CnC;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,QAAQ,CAC7B,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC,CAqJtD,CAAC"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/flows/events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAqEnC;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,QAAQ,CAC7B,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC,CAuItD,CAAC"}
package/dist/index.js CHANGED
@@ -144,6 +144,7 @@ var events = {
144
144
  signInThrottled: event("janus.signIn.throttled"),
145
145
  secondFactorAsked: event("janus.signIn.secondFactor"),
146
146
  signInCodeSent: event("janus.signInCode.sent"),
147
+ magicLinkSent: event("janus.magicLink.sent"),
147
148
  stepUpAsked: event("janus.stepUp.asked"),
148
149
  stepUpConfirmed: event("janus.stepUp.confirmed"),
149
150
  stepUpRefused: event("janus.stepUp.refused"),
@@ -161,6 +162,20 @@ var events = {
161
162
  passwordReset: event("janus.password.reset"),
162
163
  emailVerified: event("janus.email.verified")
163
164
  };
165
+ function emailedSignIn(mark) {
166
+ return (call, outcome) => {
167
+ if (!outcome.ok) {
168
+ log.warn(events.signInRefused({
169
+ ...refusalFields(call, outcome.refusal),
170
+ [mark]: true
171
+ }));
172
+ } else if (statusOf(outcome.value) === "secondFactor") {
173
+ log.info(events.secondFactorAsked(secondFactorFields(call, outcome.value)));
174
+ } else {
175
+ log.info(events.signedIn({ ...userFields(call, outcome.value), [mark]: true }));
176
+ }
177
+ };
178
+ }
164
179
  var WRITTEN = {
165
180
  signUp: (call, outcome) => {
166
181
  if (outcome.ok)
@@ -185,21 +200,13 @@ var WRITTEN = {
185
200
  log.info(events.signInCodeSent(userFields(call, outcome.value)));
186
201
  }
187
202
  },
188
- "signInCode.confirm": (call, outcome) => {
189
- if (!outcome.ok) {
190
- log.warn(events.signInRefused({
191
- ...refusalFields(call, outcome.refusal),
192
- "janus.signIn.code": true
193
- }));
194
- } else if (statusOf(outcome.value) === "secondFactor") {
195
- log.info(events.secondFactorAsked(secondFactorFields(call, outcome.value)));
196
- } else {
197
- log.info(events.signedIn({
198
- ...userFields(call, outcome.value),
199
- "janus.signIn.code": true
200
- }));
203
+ "signInCode.confirm": emailedSignIn("janus.signIn.code"),
204
+ "magicLink.request": (call, outcome) => {
205
+ if (outcome.ok && outcome.value !== null) {
206
+ log.info(events.magicLinkSent(userFields(call, outcome.value)));
201
207
  }
202
208
  },
209
+ "magicLink.confirm": emailedSignIn("janus.signIn.magicLink"),
203
210
  "stepUp.request": (call, outcome) => {
204
211
  if (outcome.ok) {
205
212
  log.info(events.stepUpAsked({
@@ -303,12 +310,13 @@ function answered(scope, call, outcome) {
303
310
  if (typeof value === "object" && value !== null && "renewed" in value) {
304
311
  scope.attribute("janus.session.renewed", value.renewed === true);
305
312
  }
306
- const status = call.flow === "signIn" || call.flow === "signInCode.confirm" ? statusOf(value) : undefined;
313
+ const status = SIGN_INS.has(call.flow) ? statusOf(value) : undefined;
307
314
  if (status !== undefined)
308
315
  scope.attribute("janus.signIn.status", status);
309
316
  }
310
317
  WRITTEN[call.flow]?.(call, outcome);
311
318
  }
319
+ var SIGN_INS = new Set(["signIn", "signInCode.confirm", "magicLink.confirm"]);
312
320
  var UNTRACED = new Set(["cookie"]);
313
321
  function traceObject(target, path, split) {
314
322
  const copy = {};
@@ -416,5 +424,5 @@ export {
416
424
  instrumentPermissions
417
425
  };
418
426
 
419
- //# debugId=37674BAFEA8C7CF764756E2164756E21
427
+ //# debugId=50A8F00D956A52CF64756E2164756E21
420
428
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -3,13 +3,13 @@
3
3
  "sources": ["../src/traced.ts", "../src/flows/events.ts", "../src/flows/fields.ts", "../src/flows/trace.ts", "../src/flows/index.ts", "../src/permissions.ts"],
4
4
  "sourcesContent": [
5
5
  "import { JanusError, type JanusErrorCode } from '@nxgt/janus';\nimport { type SpanScope, span } from '@nxgt/telemetry';\n\n/**\n * What each code is. A **failure** is the server's to fix, and fails its span;\n * a **refusal** — a wrong password, a taken login, a spent token — is an\n * answer, and leaves it `ok`. Exhaustive, so a new code does not compile until\n * it is classified.\n */\nfunction kindOf(code: JanusErrorCode): 'failure' | 'refusal' {\n\tswitch (code) {\n\t\tcase 'STORE_FAILED':\n\t\tcase 'UNSUPPORTED':\n\t\tcase 'PERMISSION_DEPTH':\n\t\tcase 'HASH_UNSUPPORTED':\n\t\t\treturn 'failure';\n\t\tcase 'NOT_FOUND':\n\t\tcase 'LOGIN_TAKEN':\n\t\tcase 'VERSION_CONFLICT':\n\t\tcase 'USER_INVALID':\n\t\tcase 'PASSWORD_TOO_SHORT':\n\t\tcase 'CREDENTIALS_INVALID':\n\t\tcase 'USER_INACTIVE':\n\t\tcase 'STEP_UP_REQUIRED':\n\t\tcase 'TOKEN_UNKNOWN':\n\t\tcase 'TOKEN_SPENT':\n\t\tcase 'TOKEN_EXPIRED':\n\t\tcase 'TOKEN_STALE':\n\t\tcase 'CODE_INVALID':\n\t\tcase 'SECOND_FACTOR_NOT_ENROLLED':\n\t\tcase 'SECOND_FACTOR_ACTIVE':\n\t\tcase 'INVALID_CURSOR':\n\t\t\treturn 'refusal';\n\t}\n}\n\n/** A `JanusError` the caller caused; anything else, or a failure code, is not. */\nexport function isRefusal(error: unknown): error is JanusError {\n\treturn error instanceof JanusError && kindOf(error.code) === 'refusal';\n}\n\n/** What a span or an event carries: scalars only, and never an absent one. */\nexport type Fields = Readonly<Record<string, string | number | boolean>>;\n\n/** `fields` without its `undefined` values. */\nexport function fieldsOf(\n\tfields: Readonly<Record<string, string | number | boolean | undefined>>,\n): Fields {\n\tconst kept: Record<string, string | number | boolean> = {};\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (value !== undefined) kept[key] = value;\n\t}\n\treturn kept;\n}\n\n/** The `id` of a user reference, a subject or an object — or nothing. */\nexport function idOf(value: unknown): string | undefined {\n\tif (typeof value === 'string') return value;\n\tif (typeof value === 'object' && value !== null && 'id' in value) {\n\t\tconst id: unknown = value.id;\n\t\tif (typeof id === 'string') return id;\n\t}\n\treturn undefined;\n}\n\n/** The `type` of a subject or an object — or nothing. */\nexport function typeOf(value: unknown): string | undefined {\n\tif (typeof value === 'object' && value !== null && 'type' in value) {\n\t\tconst type: unknown = value.type;\n\t\tif (typeof type === 'string') return type;\n\t}\n\treturn undefined;\n}\n\n/** How a traced call ended, for what is written after it. */\nexport type Outcome =\n\t| { readonly ok: true; readonly value: unknown }\n\t| { readonly ok: false; readonly refusal: JanusError };\n\n/**\n * Runs `call` in a span. **A refusal is an answer**: the span ends `ok`, with\n * `janus.refusal` set to its code, and the error is rethrown once the span is\n * over. A failure is rethrown inside it, so the span is `error`, with the\n * store slot and operation a `StoreFailure` names.\n *\n * `after` sees the outcome inside the span, outside the flow's `try`, and a\n * throw from it is dropped: telemetry never changes what a flow answers.\n */\nexport async function traced<T>(\n\tname: string,\n\tfields: Fields,\n\tcall: () => Promise<T>,\n\tafter: (scope: SpanScope, outcome: Outcome) => void,\n): Promise<T> {\n\tconst ended = await span(name, { attributes: fields }, async (scope) => {\n\t\tlet outcome: { ok: true; value: T } | { ok: false; refusal: JanusError };\n\t\ttry {\n\t\t\toutcome = { ok: true, value: await call() };\n\t\t} catch (error) {\n\t\t\tif (error instanceof JanusError) {\n\t\t\t\tscope.attribute('janus.error.code', error.code);\n\t\t\t\tif (error.slot !== undefined)\n\t\t\t\t\tscope.attribute('janus.store.slot', error.slot);\n\t\t\t\tif (error.operation !== undefined) {\n\t\t\t\t\tscope.attribute('janus.store.operation', error.operation);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!isRefusal(error)) throw error;\n\t\t\tscope.attribute('janus.refusal', error.code);\n\t\t\toutcome = { ok: false, refusal: error };\n\t\t}\n\t\ttry {\n\t\t\tafter(scope, outcome);\n\t\t} catch {\n\t\t\t// A signal is never worth a flow's answer.\n\t\t}\n\t\treturn outcome;\n\t});\n\tif (!ended.ok) throw ended.refusal;\n\treturn ended.value;\n}\n",
6
- "import { createLogger, event } from '@nxgt/telemetry';\nimport type { Outcome } from '../traced';\nimport type { Call } from './call';\nimport {\n\targumentUser,\n\trecoveryCodesLeft,\n\trefusalFields,\n\tsecondFactorFields,\n\tsessionUserFields,\n\tstatusOf,\n\tuserFields,\n\tviaOf,\n} from './fields';\n\nconst log = createLogger('@nxgt/janus');\n\n/** The events a security review reads: who signed in, out, up, stepped up, and what changed. */\nconst events = {\n\tsignedUp: event('janus.signUp'),\n\tsignedIn: event('janus.signIn'),\n\tsignInRefused: event('janus.signIn.refused'),\n\tsignInThrottled: event('janus.signIn.throttled'),\n\tsecondFactorAsked: event('janus.signIn.secondFactor'),\n\tsignInCodeSent: event('janus.signInCode.sent'),\n\tstepUpAsked: event('janus.stepUp.asked'),\n\tstepUpConfirmed: event('janus.stepUp.confirmed'),\n\tstepUpRefused: event('janus.stepUp.refused'),\n\tsecondFactorEnrolled: event('janus.secondFactor.enrolled'),\n\tsecondFactorActivated: event('janus.secondFactor.activated'),\n\tsecondFactorDisabled: event('janus.secondFactor.disabled'),\n\trecoveryCodesRegenerated: event(\n\t\t'janus.secondFactor.recoveryCodesRegenerated',\n\t),\n\tsignedOut: event('janus.signOut'),\n\tsignedOutEverywhere: event('janus.signOutEverywhere'),\n\tuserDeleted: event('janus.user.deleted'),\n\tuserActivated: event('janus.user.activated'),\n\tuserDeactivated: event('janus.user.deactivated'),\n\tpasswordChanged: event('janus.password.changed'),\n\tpasswordSet: event('janus.password.set'),\n\tpasswordReset: event('janus.password.reset'),\n\temailVerified: event('janus.email.verified'),\n};\n\n/**\n * What each flow writes once it answered. Nothing here reads a login, an\n * e-mail, a password or a token: only user types, ids, codes and reasons.\n */\nexport const WRITTEN: Readonly<\n\tRecord<string, (call: Call, outcome: Outcome) => void>\n> = {\n\tsignUp: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.signedUp(userFields(call, outcome.value)));\n\t},\n\tsignIn: (call, outcome) => {\n\t\tif (!outcome.ok && outcome.refusal.reason === 'throttled') {\n\t\t\t// Too many passwords at one login: no password was compared.\n\t\t\tlog.warn(\n\t\t\t\tevents.signInThrottled({\n\t\t\t\t\t...refusalFields(call, outcome.refusal),\n\t\t\t\t\t'janus.signIn.retryAfter': outcome.refusal.retryAfter,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (!outcome.ok) {\n\t\t\tlog.warn(events.signInRefused(refusalFields(call, outcome.refusal)));\n\t\t} else if (statusOf(outcome.value) === 'secondFactor') {\n\t\t\t// The password was right; the session waits for a code.\n\t\t\tlog.info(\n\t\t\t\tevents.secondFactorAsked(secondFactorFields(call, outcome.value)),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.info(events.signedIn(userFields(call, outcome.value)));\n\t\t}\n\t},\n\t'signInCode.request': (call, outcome) => {\n\t\t// Only when a code was issued: `null` is nobody, and says nothing.\n\t\tif (outcome.ok && outcome.value !== null) {\n\t\t\tlog.info(events.signInCodeSent(userFields(call, outcome.value)));\n\t\t}\n\t},\n\t'signInCode.confirm': (call, outcome) => {\n\t\tif (!outcome.ok) {\n\t\t\t// Marked, so an alert on burnt challenges can tell the two codes apart.\n\t\t\tlog.warn(\n\t\t\t\tevents.signInRefused({\n\t\t\t\t\t...refusalFields(call, outcome.refusal),\n\t\t\t\t\t'janus.signIn.code': true,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (statusOf(outcome.value) === 'secondFactor') {\n\t\t\tlog.info(\n\t\t\t\tevents.secondFactorAsked(secondFactorFields(call, outcome.value)),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.info(\n\t\t\t\tevents.signedIn({\n\t\t\t\t\t...userFields(call, outcome.value),\n\t\t\t\t\t'janus.signIn.code': true,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t},\n\t'stepUp.request': (call, outcome) => {\n\t\t// Which code confirms it — e-mailed, or from the app — never the code.\n\t\tif (outcome.ok) {\n\t\t\tlog.info(\n\t\t\t\tevents.stepUpAsked({\n\t\t\t\t\t...userFields(call, outcome.value),\n\t\t\t\t\t'janus.stepUp.via': viaOf(outcome.value),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t},\n\t'stepUp.confirm': (call, outcome) => {\n\t\tif (outcome.ok) {\n\t\t\tlog.info(events.stepUpConfirmed(sessionUserFields(call, outcome.value)));\n\t\t} else {\n\t\t\tlog.warn(events.stepUpRefused(refusalFields(call, outcome.refusal)));\n\t\t}\n\t},\n\t'secondFactor.confirm': (call, outcome) => {\n\t\tif (outcome.ok) {\n\t\t\tlog.info(\n\t\t\t\tevents.signedIn({\n\t\t\t\t\t...userFields(call, outcome.value),\n\t\t\t\t\t'janus.signIn.secondFactor': true,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.warn(events.signInRefused(refusalFields(call, outcome.refusal)));\n\t\t}\n\t},\n\t'secondFactor.recover': (call, outcome) => {\n\t\t// Marked, so an alert can watch sign-ins made without the phone.\n\t\tif (outcome.ok) {\n\t\t\tlog.info(\n\t\t\t\tevents.signedIn({\n\t\t\t\t\t...userFields(call, outcome.value),\n\t\t\t\t\t'janus.signIn.recoveryCode': true,\n\t\t\t\t\t'janus.secondFactor.recoveryCodesLeft': recoveryCodesLeft(\n\t\t\t\t\t\toutcome.value,\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.warn(\n\t\t\t\tevents.signInRefused({\n\t\t\t\t\t...refusalFields(call, outcome.refusal),\n\t\t\t\t\t'janus.signIn.recoveryCode': true,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t},\n\t'secondFactor.regenerateRecoveryCodes': (call, outcome) => {\n\t\tif (outcome.ok) {\n\t\t\tlog.info(events.recoveryCodesRegenerated(argumentUser(call)));\n\t\t}\n\t},\n\t'secondFactor.enroll': (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.secondFactorEnrolled(argumentUser(call)));\n\t},\n\t'secondFactor.activate': (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.secondFactorActivated(argumentUser(call)));\n\t},\n\t'secondFactor.disable': (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.secondFactorDisabled(argumentUser(call)));\n\t},\n\tsignOut: (_call, outcome) => {\n\t\tif (outcome.ok && outcome.value === true) log.info(events.signedOut());\n\t},\n\tsignOutEverywhere: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.signedOutEverywhere(argumentUser(call)));\n\t},\n\tdelete: (call, outcome) => {\n\t\tif (outcome.ok && outcome.value === true) {\n\t\t\tlog.info(events.userDeleted(argumentUser(call)));\n\t\t}\n\t},\n\tsetActive: (call, outcome) => {\n\t\tif (!outcome.ok) return;\n\t\tconst written =\n\t\t\tcall.args[1] === true ? events.userActivated : events.userDeactivated;\n\t\tlog.info(written(argumentUser(call)));\n\t},\n\tchangePassword: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.passwordChanged(argumentUser(call)));\n\t},\n\tsetPassword: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.passwordSet(argumentUser(call)));\n\t},\n\t'resetPassword.confirm': (call, outcome) => {\n\t\tif (outcome.ok)\n\t\t\tlog.info(events.passwordReset(userFields(call, outcome.value)));\n\t},\n\t'verifyEmail.confirm': (call, outcome) => {\n\t\tif (outcome.ok)\n\t\t\tlog.info(events.emailVerified(userFields(call, outcome.value)));\n\t},\n};\n",
6
+ "import { createLogger, event } from '@nxgt/telemetry';\nimport type { Outcome } from '../traced';\nimport type { Call } from './call';\nimport {\n\targumentUser,\n\trecoveryCodesLeft,\n\trefusalFields,\n\tsecondFactorFields,\n\tsessionUserFields,\n\tstatusOf,\n\tuserFields,\n\tviaOf,\n} from './fields';\n\nconst log = createLogger('@nxgt/janus');\n\n/** The events a security review reads: who signed in, out, up, stepped up, and what changed. */\nconst events = {\n\tsignedUp: event('janus.signUp'),\n\tsignedIn: event('janus.signIn'),\n\tsignInRefused: event('janus.signIn.refused'),\n\tsignInThrottled: event('janus.signIn.throttled'),\n\tsecondFactorAsked: event('janus.signIn.secondFactor'),\n\tsignInCodeSent: event('janus.signInCode.sent'),\n\tmagicLinkSent: event('janus.magicLink.sent'),\n\tstepUpAsked: event('janus.stepUp.asked'),\n\tstepUpConfirmed: event('janus.stepUp.confirmed'),\n\tstepUpRefused: event('janus.stepUp.refused'),\n\tsecondFactorEnrolled: event('janus.secondFactor.enrolled'),\n\tsecondFactorActivated: event('janus.secondFactor.activated'),\n\tsecondFactorDisabled: event('janus.secondFactor.disabled'),\n\trecoveryCodesRegenerated: event(\n\t\t'janus.secondFactor.recoveryCodesRegenerated',\n\t),\n\tsignedOut: event('janus.signOut'),\n\tsignedOutEverywhere: event('janus.signOutEverywhere'),\n\tuserDeleted: event('janus.user.deleted'),\n\tuserActivated: event('janus.user.activated'),\n\tuserDeactivated: event('janus.user.deactivated'),\n\tpasswordChanged: event('janus.password.changed'),\n\tpasswordSet: event('janus.password.set'),\n\tpasswordReset: event('janus.password.reset'),\n\temailVerified: event('janus.email.verified'),\n};\n\n/**\n * What a sign-in by e-mail writes once confirmed — a code or a link —\n * marked, so an alert on refusals can tell one from the other and from a\n * password: a refusal, the second factor asked for, or a sign-in.\n */\nfunction emailedSignIn(mark: string): (call: Call, outcome: Outcome) => void {\n\treturn (call, outcome) => {\n\t\tif (!outcome.ok) {\n\t\t\tlog.warn(\n\t\t\t\tevents.signInRefused({\n\t\t\t\t\t...refusalFields(call, outcome.refusal),\n\t\t\t\t\t[mark]: true,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (statusOf(outcome.value) === 'secondFactor') {\n\t\t\tlog.info(\n\t\t\t\tevents.secondFactorAsked(secondFactorFields(call, outcome.value)),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.info(\n\t\t\t\tevents.signedIn({ ...userFields(call, outcome.value), [mark]: true }),\n\t\t\t);\n\t\t}\n\t};\n}\n\n/**\n * What each flow writes once it answered. Nothing here reads a login, an\n * e-mail, a password or a token: only user types, ids, codes and reasons.\n */\nexport const WRITTEN: Readonly<\n\tRecord<string, (call: Call, outcome: Outcome) => void>\n> = {\n\tsignUp: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.signedUp(userFields(call, outcome.value)));\n\t},\n\tsignIn: (call, outcome) => {\n\t\tif (!outcome.ok && outcome.refusal.reason === 'throttled') {\n\t\t\t// Too many passwords at one login: no password was compared.\n\t\t\tlog.warn(\n\t\t\t\tevents.signInThrottled({\n\t\t\t\t\t...refusalFields(call, outcome.refusal),\n\t\t\t\t\t'janus.signIn.retryAfter': outcome.refusal.retryAfter,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (!outcome.ok) {\n\t\t\tlog.warn(events.signInRefused(refusalFields(call, outcome.refusal)));\n\t\t} else if (statusOf(outcome.value) === 'secondFactor') {\n\t\t\t// The password was right; the session waits for a code.\n\t\t\tlog.info(\n\t\t\t\tevents.secondFactorAsked(secondFactorFields(call, outcome.value)),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.info(events.signedIn(userFields(call, outcome.value)));\n\t\t}\n\t},\n\t'signInCode.request': (call, outcome) => {\n\t\t// Only when a code was issued: `null` is nobody, and says nothing.\n\t\tif (outcome.ok && outcome.value !== null) {\n\t\t\tlog.info(events.signInCodeSent(userFields(call, outcome.value)));\n\t\t}\n\t},\n\t'signInCode.confirm': emailedSignIn('janus.signIn.code'),\n\t'magicLink.request': (call, outcome) => {\n\t\t// Only when a link was issued: `null` is nobody, and says nothing.\n\t\tif (outcome.ok && outcome.value !== null) {\n\t\t\tlog.info(events.magicLinkSent(userFields(call, outcome.value)));\n\t\t}\n\t},\n\t'magicLink.confirm': emailedSignIn('janus.signIn.magicLink'),\n\t'stepUp.request': (call, outcome) => {\n\t\t// Which code confirms it — e-mailed, or from the app — never the code.\n\t\tif (outcome.ok) {\n\t\t\tlog.info(\n\t\t\t\tevents.stepUpAsked({\n\t\t\t\t\t...userFields(call, outcome.value),\n\t\t\t\t\t'janus.stepUp.via': viaOf(outcome.value),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t},\n\t'stepUp.confirm': (call, outcome) => {\n\t\tif (outcome.ok) {\n\t\t\tlog.info(events.stepUpConfirmed(sessionUserFields(call, outcome.value)));\n\t\t} else {\n\t\t\tlog.warn(events.stepUpRefused(refusalFields(call, outcome.refusal)));\n\t\t}\n\t},\n\t'secondFactor.confirm': (call, outcome) => {\n\t\tif (outcome.ok) {\n\t\t\tlog.info(\n\t\t\t\tevents.signedIn({\n\t\t\t\t\t...userFields(call, outcome.value),\n\t\t\t\t\t'janus.signIn.secondFactor': true,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.warn(events.signInRefused(refusalFields(call, outcome.refusal)));\n\t\t}\n\t},\n\t'secondFactor.recover': (call, outcome) => {\n\t\t// Marked, so an alert can watch sign-ins made without the phone.\n\t\tif (outcome.ok) {\n\t\t\tlog.info(\n\t\t\t\tevents.signedIn({\n\t\t\t\t\t...userFields(call, outcome.value),\n\t\t\t\t\t'janus.signIn.recoveryCode': true,\n\t\t\t\t\t'janus.secondFactor.recoveryCodesLeft': recoveryCodesLeft(\n\t\t\t\t\t\toutcome.value,\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tlog.warn(\n\t\t\t\tevents.signInRefused({\n\t\t\t\t\t...refusalFields(call, outcome.refusal),\n\t\t\t\t\t'janus.signIn.recoveryCode': true,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t},\n\t'secondFactor.regenerateRecoveryCodes': (call, outcome) => {\n\t\tif (outcome.ok) {\n\t\t\tlog.info(events.recoveryCodesRegenerated(argumentUser(call)));\n\t\t}\n\t},\n\t'secondFactor.enroll': (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.secondFactorEnrolled(argumentUser(call)));\n\t},\n\t'secondFactor.activate': (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.secondFactorActivated(argumentUser(call)));\n\t},\n\t'secondFactor.disable': (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.secondFactorDisabled(argumentUser(call)));\n\t},\n\tsignOut: (_call, outcome) => {\n\t\tif (outcome.ok && outcome.value === true) log.info(events.signedOut());\n\t},\n\tsignOutEverywhere: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.signedOutEverywhere(argumentUser(call)));\n\t},\n\tdelete: (call, outcome) => {\n\t\tif (outcome.ok && outcome.value === true) {\n\t\t\tlog.info(events.userDeleted(argumentUser(call)));\n\t\t}\n\t},\n\tsetActive: (call, outcome) => {\n\t\tif (!outcome.ok) return;\n\t\tconst written =\n\t\t\tcall.args[1] === true ? events.userActivated : events.userDeactivated;\n\t\tlog.info(written(argumentUser(call)));\n\t},\n\tchangePassword: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.passwordChanged(argumentUser(call)));\n\t},\n\tsetPassword: (call, outcome) => {\n\t\tif (outcome.ok) log.info(events.passwordSet(argumentUser(call)));\n\t},\n\t'resetPassword.confirm': (call, outcome) => {\n\t\tif (outcome.ok)\n\t\t\tlog.info(events.passwordReset(userFields(call, outcome.value)));\n\t},\n\t'verifyEmail.confirm': (call, outcome) => {\n\t\tif (outcome.ok)\n\t\t\tlog.info(events.emailVerified(userFields(call, outcome.value)));\n\t},\n};\n",
7
7
  "import type { JanusError } from '@nxgt/janus';\nimport { type Fields, fieldsOf, idOf, typeOf } from '../traced';\nimport type { Call } from './call';\n\n/** The user an answer names — `{ user }` or the user itself — as fields. */\nexport function userFields(call: Call, value: unknown): Fields {\n\tconst user =\n\t\ttypeof value === 'object' && value !== null && 'user' in value\n\t\t\t? value.user\n\t\t\t: value;\n\treturn fieldsOf({\n\t\t'janus.user.type': typeOf(user) ?? call.userType,\n\t\t'user.id': idOf(user),\n\t});\n}\n\n/** Whose sign-in waits for a code: the `userId` the answer carries. */\nexport function secondFactorFields(call: Call, value: unknown): Fields {\n\tconst userId =\n\t\ttypeof value === 'object' && value !== null && 'userId' in value\n\t\t\t? value.userId\n\t\t\t: undefined;\n\treturn fieldsOf({\n\t\t'janus.user.type': call.userType,\n\t\t'user.id': typeof userId === 'string' ? userId : undefined,\n\t});\n}\n\n/** The user a flow was called for: its first argument. */\nexport function argumentUser(call: Call): Fields {\n\treturn fieldsOf({\n\t\t'janus.user.type': call.userType,\n\t\t'user.id': idOf(call.args[0]),\n\t});\n}\n\n/**\n * Why a sign-in was refused: the code, the reason, the user when one holds\n * the login, and what a challenge has left of its attempts.\n */\nexport function refusalFields(call: Call, refusal: JanusError): Fields {\n\treturn fieldsOf({\n\t\t'janus.user.type': refusal.userType ?? call.userType,\n\t\t'janus.refusal': refusal.code,\n\t\t'janus.refusal.reason': refusal.reason,\n\t\t'janus.secondFactor.attemptsLeft': refusal.attemptsLeft,\n\t\t'user.id': refusal.userId,\n\t});\n}\n\n/** `signIn`'s `status`, when the answer has one. */\nexport function statusOf(value: unknown): string | undefined {\n\treturn typeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'status' in value &&\n\t\ttypeof value.status === 'string'\n\t\t? value.status\n\t\t: undefined;\n}\n\n/** How many recovery codes `secondFactor.recover` left: a count, never a code. */\nexport function recoveryCodesLeft(value: unknown): number | undefined {\n\treturn typeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'recoveryCodesLeft' in value &&\n\t\ttypeof value.recoveryCodesLeft === 'number'\n\t\t? value.recoveryCodesLeft\n\t\t: undefined;\n}\n\n/**\n * The user a session names — what `stepUp.confirm` answers — by its\n * `userId`: the session's own id is not the user's.\n */\nexport function sessionUserFields(call: Call, value: unknown): Fields {\n\tconst userId =\n\t\ttypeof value === 'object' && value !== null && 'userId' in value\n\t\t\t? value.userId\n\t\t\t: undefined;\n\treturn fieldsOf({\n\t\t'janus.user.type': call.userType,\n\t\t'user.id': typeof userId === 'string' ? userId : undefined,\n\t});\n}\n\n/** How a step-up is confirmed — `email` or `secondFactor` — when the answer says. */\nexport function viaOf(value: unknown): string | undefined {\n\treturn typeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'via' in value &&\n\t\ttypeof value.via === 'string'\n\t\t? value.via\n\t\t: undefined;\n}\n",
8
- "import type { SpanScope } from '@nxgt/telemetry';\nimport { fieldsOf, type Outcome, traced } from '../traced';\nimport type { Call } from './call';\nimport { WRITTEN } from './events';\nimport { sessionUserFields, statusOf, userFields } from './fields';\n\n/** What a span learns from an answer: whose it is, never what it holds. */\nfunction answered(scope: SpanScope, call: Call, outcome: Outcome): void {\n\tif (outcome.ok) {\n\t\t// A session is answered by `stepUp.confirm`: its user is its `userId`.\n\t\tconst fields =\n\t\t\tcall.flow === 'stepUp.confirm'\n\t\t\t\t? sessionUserFields(call, outcome.value)\n\t\t\t\t: userFields(call, outcome.value);\n\t\tfor (const [key, value] of Object.entries(fields))\n\t\t\tscope.attribute(key, value);\n\t\tconst value = outcome.value;\n\t\tif (typeof value === 'object' && value !== null && 'renewed' in value) {\n\t\t\tscope.attribute('janus.session.renewed', value.renewed === true);\n\t\t}\n\t\tconst status =\n\t\t\tcall.flow === 'signIn' || call.flow === 'signInCode.confirm'\n\t\t\t\t? statusOf(value)\n\t\t\t\t: undefined;\n\t\tif (status !== undefined) scope.attribute('janus.signIn.status', status);\n\t}\n\tWRITTEN[call.flow]?.(call, outcome);\n}\n\n/** `cookie` answers synchronously: it is the one part of `janus()` not traced. */\nconst UNTRACED = new Set(['cookie']);\n\n/** Which user type and flow a path names: `['patient', 'signIn']`. */\ntype Split = (path: readonly string[]) => {\n\treadonly userType: string | undefined;\n\treadonly flow: readonly string[];\n};\n\n/**\n * A traced copy of `target`, frozen: its functions traced, its plain objects —\n * a user type's flows, `verifyEmail` — copied the same way. A copy rather\n * than a Proxy, which may not answer differently for a frozen property.\n */\nexport function traceObject<O extends object>(\n\ttarget: O,\n\tpath: readonly string[],\n\tsplit: Split,\n): O {\n\tconst copy: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(target)) {\n\t\tif (UNTRACED.has(key)) {\n\t\t\tcopy[key] = value;\n\t\t} else if (typeof value === 'function') {\n\t\t\tcopy[key] = traceFunction(\n\t\t\t\tvalue as (...args: unknown[]) => unknown,\n\t\t\t\ttarget,\n\t\t\t\t[...path, key],\n\t\t\t\tsplit,\n\t\t\t);\n\t\t} else if (\n\t\t\ttypeof value === 'object' &&\n\t\t\tvalue !== null &&\n\t\t\t!Array.isArray(value)\n\t\t) {\n\t\t\tcopy[key] = traceObject(value, [...path, key], split);\n\t\t} else {\n\t\t\tcopy[key] = value;\n\t\t}\n\t}\n\treturn Object.freeze(copy) as O;\n}\n\nfunction traceFunction(\n\tfn: (...args: unknown[]) => unknown,\n\tself: object,\n\tpath: readonly string[],\n\tsplit: Split,\n): (...args: unknown[]) => Promise<unknown> {\n\tconst { userType, flow } = split(path);\n\tconst name = `janus.${path.join('.')}`;\n\tconst fields = fieldsOf({ 'janus.user.type': userType });\n\treturn (...args) => {\n\t\tconst call: Call = { flow: flow.join('.'), userType, args };\n\t\treturn traced(\n\t\t\tname,\n\t\t\tfields,\n\t\t\t() => Promise.resolve(fn.apply(self, args)),\n\t\t\t(scope, outcome) => answered(scope, call, outcome),\n\t\t);\n\t};\n}\n",
8
+ "import type { SpanScope } from '@nxgt/telemetry';\nimport { fieldsOf, type Outcome, traced } from '../traced';\nimport type { Call } from './call';\nimport { WRITTEN } from './events';\nimport { sessionUserFields, statusOf, userFields } from './fields';\n\n/** What a span learns from an answer: whose it is, never what it holds. */\nfunction answered(scope: SpanScope, call: Call, outcome: Outcome): void {\n\tif (outcome.ok) {\n\t\t// A session is answered by `stepUp.confirm`: its user is its `userId`.\n\t\tconst fields =\n\t\t\tcall.flow === 'stepUp.confirm'\n\t\t\t\t? sessionUserFields(call, outcome.value)\n\t\t\t\t: userFields(call, outcome.value);\n\t\tfor (const [key, value] of Object.entries(fields))\n\t\t\tscope.attribute(key, value);\n\t\tconst value = outcome.value;\n\t\tif (typeof value === 'object' && value !== null && 'renewed' in value) {\n\t\t\tscope.attribute('janus.session.renewed', value.renewed === true);\n\t\t}\n\t\tconst status = SIGN_INS.has(call.flow) ? statusOf(value) : undefined;\n\t\tif (status !== undefined) scope.attribute('janus.signIn.status', status);\n\t}\n\tWRITTEN[call.flow]?.(call, outcome);\n}\n\n/** The flows whose answer is a sign-in's: a session, or a second factor asked for. */\nconst SIGN_INS = new Set(['signIn', 'signInCode.confirm', 'magicLink.confirm']);\n\n/** `cookie` answers synchronously: it is the one part of `janus()` not traced. */\nconst UNTRACED = new Set(['cookie']);\n\n/** Which user type and flow a path names: `['patient', 'signIn']`. */\ntype Split = (path: readonly string[]) => {\n\treadonly userType: string | undefined;\n\treadonly flow: readonly string[];\n};\n\n/**\n * A traced copy of `target`, frozen: its functions traced, its plain objects —\n * a user type's flows, `verifyEmail` — copied the same way. A copy rather\n * than a Proxy, which may not answer differently for a frozen property.\n */\nexport function traceObject<O extends object>(\n\ttarget: O,\n\tpath: readonly string[],\n\tsplit: Split,\n): O {\n\tconst copy: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(target)) {\n\t\tif (UNTRACED.has(key)) {\n\t\t\tcopy[key] = value;\n\t\t} else if (typeof value === 'function') {\n\t\t\tcopy[key] = traceFunction(\n\t\t\t\tvalue as (...args: unknown[]) => unknown,\n\t\t\t\ttarget,\n\t\t\t\t[...path, key],\n\t\t\t\tsplit,\n\t\t\t);\n\t\t} else if (\n\t\t\ttypeof value === 'object' &&\n\t\t\tvalue !== null &&\n\t\t\t!Array.isArray(value)\n\t\t) {\n\t\t\tcopy[key] = traceObject(value, [...path, key], split);\n\t\t} else {\n\t\t\tcopy[key] = value;\n\t\t}\n\t}\n\treturn Object.freeze(copy) as O;\n}\n\nfunction traceFunction(\n\tfn: (...args: unknown[]) => unknown,\n\tself: object,\n\tpath: readonly string[],\n\tsplit: Split,\n): (...args: unknown[]) => Promise<unknown> {\n\tconst { userType, flow } = split(path);\n\tconst name = `janus.${path.join('.')}`;\n\tconst fields = fieldsOf({ 'janus.user.type': userType });\n\treturn (...args) => {\n\t\tconst call: Call = { flow: flow.join('.'), userType, args };\n\t\treturn traced(\n\t\t\tname,\n\t\t\tfields,\n\t\t\t() => Promise.resolve(fn.apply(self, args)),\n\t\t\t(scope, outcome) => answered(scope, call, outcome),\n\t\t);\n\t};\n}\n",
9
9
  "import { traceObject } from './trace';\n\n/** What `janus()` answers, as far as it is read: its user types and its flows. */\nexport interface JanusLike {\n\treadonly types: readonly string[];\n\treadonly authenticate: (...args: never[]) => Promise<unknown>;\n}\n\n/**\n * The same `janus()` instance, with every flow traced — a span per call,\n * named `janus.signIn` or `janus.patient.signIn` — and the events a security\n * review reads written as logs: sign-ups, sign-ins and the reason one was\n * refused, a second factor asked for, enrolled, activated or disabled, a\n * sign-in by recovery code and the codes left, recovery codes regenerated,\n * sign-outs, deleted and deactivated users, changed passwords.\n *\n * ```ts\n * const auth = instrumentJanus(janus({ users, store, hasher }));\n * ```\n *\n * **A refusal is an answer**: a wrong password leaves its span `ok`, with\n * `janus.refusal` set, and is a `janus.signIn.refused` warning — never a\n * failed span. A store that cannot answer is one, with `janus.store.slot`.\n *\n * Nothing written carries a login, an e-mail, a password, a token, a\n * challenge, a code, a TOTP secret or a session id: user types, user ids,\n * codes and reasons only.\n */\nexport function instrumentJanus<A extends JanusLike>(auth: A): A {\n\tconst types = auth.types.filter((type) => typeof type === 'string');\n\tconst only = types.length === 1 ? types[0] : undefined;\n\treturn traceObject(auth, [], (path) =>\n\t\tpath[0] !== undefined && types.includes(path[0])\n\t\t\t? { userType: path[0], flow: path.slice(1) }\n\t\t\t: { userType: only, flow: path },\n\t);\n}\n",
10
10
  "import { isSetOf } from '@nxgt/janus';\nimport { createLogger, event, type TelemetryEvent } from '@nxgt/telemetry';\nimport { type Fields, fieldsOf, idOf, traced, typeOf } from './traced';\n\nconst log = createLogger('@nxgt/janus');\n\n/** The audit trail of who was given what: one event per tuple written. */\nconst granted = event('janus.tuple.granted');\nconst revoked = event('janus.tuple.revoked');\n\n/**\n * A subject — a user, an object, or a subject set — as fields, read as\n * `permissions()` reads it: a value of a user type is that user, even with a\n * field named `relation`, unless `setOf()` made it.\n */\nfunction subjectFields(\n\tsubject: unknown,\n\tuserTypes: ReadonlySet<string>,\n): Fields {\n\tconst type = typeOf(subject);\n\tconst relation =\n\t\ttypeof subject === 'object' && subject !== null && 'relation' in subject\n\t\t\t? subject.relation\n\t\t\t: undefined;\n\tconst isSet =\n\t\ttypeof relation === 'string' &&\n\t\t(isSetOf(subject) || type === undefined || !userTypes.has(type));\n\treturn fieldsOf({\n\t\t'janus.subject.type': type,\n\t\t'janus.subject.id': idOf(subject),\n\t\t'janus.subject.relation': isSet ? relation : undefined,\n\t});\n}\n\nfunction objectFields(object: unknown): Fields {\n\treturn fieldsOf({\n\t\t'janus.object.type': typeOf(object),\n\t\t'janus.object.id': idOf(object),\n\t});\n}\n\n/** What `permissions()` answers, as far as it is traced. */\nexport interface PermissionsLike {\n\t/** Its user types tell a user from a subject set of the same shape. */\n\treadonly model?: { readonly subjects: readonly string[] };\n\treadonly can: (...args: never[]) => Promise<boolean>;\n\treadonly list: (...args: never[]) => Promise<unknown>;\n\treadonly grant: (...args: never[]) => Promise<unknown>;\n\treadonly revoke: (...args: never[]) => Promise<unknown>;\n}\n\ntype Traced = (...args: unknown[]) => Promise<unknown>;\n\n/** How each method of `permissions()` is traced. */\nconst tracersFor = (\n\tuserTypes: ReadonlySet<string>,\n): Readonly<Record<string, (fn: Traced) => Traced>> => ({\n\tcan:\n\t\t(fn) =>\n\t\t(...args) =>\n\t\t\ttraced(\n\t\t\t\t'janus.can',\n\t\t\t\tfieldsOf({\n\t\t\t\t\t...subjectFields(args[0], userTypes),\n\t\t\t\t\t'janus.permission': typeof args[1] === 'string' ? args[1] : undefined,\n\t\t\t\t\t...objectFields(args[2]),\n\t\t\t\t}),\n\t\t\t\t() => fn(...args),\n\t\t\t\t(scope, outcome) => {\n\t\t\t\t\tif (outcome.ok)\n\t\t\t\t\t\tscope.attribute('janus.allowed', outcome.value === true);\n\t\t\t\t},\n\t\t\t),\n\tlist:\n\t\t(fn) =>\n\t\t(...args) =>\n\t\t\ttraced(\n\t\t\t\t'janus.list',\n\t\t\t\tfieldsOf({\n\t\t\t\t\t...subjectFields(args[0], userTypes),\n\t\t\t\t\t'janus.permission': typeof args[1] === 'string' ? args[1] : undefined,\n\t\t\t\t\t'janus.object.type':\n\t\t\t\t\t\ttypeof args[2] === 'string' ? args[2] : undefined,\n\t\t\t\t}),\n\t\t\t\t() => fn(...args),\n\t\t\t\t(scope, outcome) => {\n\t\t\t\t\tconst page = outcome.ok ? outcome.value : undefined;\n\t\t\t\t\tif (typeof page === 'object' && page !== null && 'items' in page) {\n\t\t\t\t\t\tconst items = page.items;\n\t\t\t\t\t\tif (Array.isArray(items))\n\t\t\t\t\t\t\tscope.attribute('janus.page.items', items.length);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t),\n\tgrant: (fn) => tupleWrite('janus.grant', granted, fn, userTypes),\n\trevoke: (fn) => tupleWrite('janus.revoke', revoked, fn, userTypes),\n});\n\nfunction tupleWrite(\n\tname: string,\n\twritten: (fields: Fields) => TelemetryEvent,\n\tfn: Traced,\n\tuserTypes: ReadonlySet<string>,\n): Traced {\n\treturn (...args) => {\n\t\tconst fields = fieldsOf({\n\t\t\t...objectFields(args[0]),\n\t\t\t'janus.relation': typeof args[1] === 'string' ? args[1] : undefined,\n\t\t\t...subjectFields(args[2], userTypes),\n\t\t});\n\t\treturn traced(\n\t\t\tname,\n\t\t\tfields,\n\t\t\t() => fn(...args),\n\t\t\t(_scope, outcome) => {\n\t\t\t\tif (outcome.ok) log.info(written(fields));\n\t\t\t},\n\t\t);\n\t};\n}\n\n/**\n * The same `permissions()` instance, traced: a span per `can` — its subject,\n * permission, object and whether it was allowed — and per `list`, and a\n * `janus.tuple.granted` or `janus.tuple.revoked` event for every tuple\n * written, the audit trail of who was given what.\n *\n * ```ts\n * const access = instrumentPermissions(permissions({ model, store }));\n * ```\n *\n * A denial is an answer, not a failure: `janus.allowed` is `false` and the\n * span is `ok`. A relation store that cannot answer fails the span.\n */\nexport function instrumentPermissions<A extends PermissionsLike>(access: A): A {\n\t// `permissions()` answers a frozen object, which a Proxy may not answer\n\t// differently for: the traced methods go on a copy, frozen in turn.\n\tconst copy: Record<string, unknown> = Object.fromEntries(\n\t\tObject.entries(access),\n\t);\n\tconst userTypes = new Set(access.model?.subjects ?? []);\n\tfor (const [key, tracer] of Object.entries(tracersFor(userTypes))) {\n\t\tconst method: unknown = copy[key];\n\t\tif (typeof method !== 'function') continue;\n\t\tcopy[key] = tracer((...args) =>\n\t\t\tPromise.resolve(method.apply(access, args)),\n\t\t);\n\t}\n\treturn Object.freeze(copy) as A;\n}\n"
11
11
  ],
12
- "mappings": "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",
13
- "debugId": "37674BAFEA8C7CF764756E2164756E21",
12
+ "mappings": "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",
13
+ "debugId": "50A8F00D956A52CF64756E2164756E21",
14
14
  "names": []
15
15
  }
@@ -235,6 +235,46 @@ inbox, or trying their luck with a challenge at a time:
235
235
  // name = 'janus.signInCode.sent', grouped by user.id, more than 10 in an hour
236
236
  ```
237
237
 
238
+ ## A link sent by e-mail
239
+
240
+ A sign-in by e-mailed link — `magicLink.request`, then `magicLink.confirm`
241
+ from the page the link opens — writes the same shape as a code, with its own
242
+ names:
243
+
244
+ ```
245
+ POST /sign-in/email/link server
246
+ └─ janus.magicLink.request janus.user.type=user user.id=0199…
247
+ log janus.magicLink.sent janus.user.type=user user.id=0199…
248
+
249
+ POST /sign-in/link server
250
+ └─ janus.magicLink.confirm janus.user.type=user user.id=0199… janus.signIn.status=signedIn
251
+ log janus.signIn user.id=0199… janus.signIn.magicLink=true
252
+ ```
253
+
254
+ `janus.magicLink.sent` is written only when a link was issued, and never
255
+ holds the token nor the address. `janus.signIn.magicLink: true` tells a
256
+ sign-in by link from one by password or by code; a user whose second factor
257
+ is active gets `janus.signIn.status=secondFactor` and a
258
+ `janus.signIn.secondFactor` event instead, as after a code.
259
+
260
+ A refused link is a `janus.signIn.refused` with its code — `TOKEN_SPENT`,
261
+ `TOKEN_EXPIRED`, `TOKEN_UNKNOWN` with no `user.id`; `TOKEN_STALE` and
262
+ `USER_INACTIVE` with one — and **every** refusal carries
263
+ `janus.signIn.magicLink: true`:
264
+
265
+ ```
266
+ janus.signIn.refused janus.refusal=TOKEN_SPENT janus.signIn.magicLink=true
267
+ ```
268
+
269
+ A link has no attempts, so there is no count to watch. What is worth an
270
+ alert is a burst of `TOKEN_SPENT` on links: a user clicks once, and a mail
271
+ scanner that opened the link before them already spent it — the sign of a
272
+ route that confirms on the link's `GET`:
273
+
274
+ ```ts
275
+ // name = 'janus.signIn.refused' and janus.signIn.magicLink = true and janus.refusal = 'TOKEN_SPENT', more than 5 in an hour
276
+ ```
277
+
238
278
  ## A step-up
239
279
 
240
280
  A step-up — a signed-in user proving again who they are before a
@@ -273,16 +313,18 @@ session worth looking at — it may not be theirs:
273
313
  ## What is never written
274
314
 
275
315
  A login, an e-mail, a password, a session token, a one-time token, a
276
- challenge — a second factor's or a sign-in code's — a code, a TOTP secret,
316
+ challenge — a second factor's or a sign-in code's — a code, a sign-in
317
+ link's token, a TOTP secret,
277
318
  the `otpauth://` URI that holds it, a session id: nothing a log reader could
278
319
  sign in with, or use to tell who holds an account. A refused sign-in by
279
320
  an unknown login says `janus.refusal.reason: 'unknownLogin'`, not which login
280
321
  was tried; only a refusal after the password was right, or against a
281
322
  challenge — `USER_INACTIVE`, `CODE_INVALID`, `TOKEN_STALE` or
282
323
  `SECOND_FACTOR_NOT_ENROLLED` — names the user. The specs that hold this run
283
- every flow — `enroll`, `activate`, a sign-in code requested for nobody and
284
- for a user, a refused and an accepted `confirm` among them — and search
285
- every signal for each code, challenge and session token they handled.
324
+ every flow — `enroll`, `activate`, a sign-in code and a sign-in link
325
+ requested for nobody and for a user, a refused and an accepted `confirm`
326
+ among them — and search every signal for each code, challenge, link token
327
+ and session token they handled.
286
328
 
287
329
  ## In a test
288
330
 
package/docs/roadmap.md CHANGED
@@ -33,6 +33,12 @@ Nothing yet.
33
33
 
34
34
  Newest first; the [CHANGELOG](../CHANGELOG.md) holds every release.
35
35
 
36
+ - **Sign-in links in the audit trail, v0.7.0** — `janus.magicLink.sent`
37
+ when `magicLink.request` issued a link, never its token nor the address;
38
+ a sign-in by link is `janus.signIn` with `janus.signIn.magicLink: true`,
39
+ and every refusal of `magicLink.confirm` a `janus.signIn.refused` with the
40
+ same mark. The `magicLink.confirm` span records `janus.signIn.status`.
41
+ Needs `@nxgt/janus` 0.15.0.
36
42
  - **Throttled sign-ins in the audit trail, v0.6.0** —
37
43
  `janus.signIn.throttled`, a warning with `janus.signIn.retryAfter`, when
38
44
  `@nxgt/janus` refuses a login past its attempts; never the login. Needs
@@ -12,6 +12,7 @@ would be without it.
12
12
  - [A wrong password does not fail the span](#a-wrong-password-does-not-fail-the-span)
13
13
  - [`janus.signIn.refused` has no `user.id`](#janussigninrefused-has-no-userid)
14
14
  - [A `janus.signInCode.request` span with no `janus.signInCode.sent` event](#a-janussignincoderequest-span-with-no-janussignincodesent-event)
15
+ - [`janus.signIn.refused` with `TOKEN_SPENT` and `janus.signIn.magicLink`, for users who clicked once](#janussigninrefused-with-token_spent-and-janussigninmagiclink-for-users-who-clicked-once)
15
16
  - [Every wrong password fails its span](#every-wrong-password-fails-its-span)
16
17
 
17
18
  ### No `janus.*` span or event at all
@@ -100,6 +101,25 @@ must not become a list of addresses tried against your users.
100
101
  find, see
101
102
  [`signInCode.request` answers `null` for a user who exists](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/troubleshooting.md#signincoderequest-answers-null-for-a-user-who-exists).
102
103
 
104
+ The same holds for a `janus.magicLink.request` span with no
105
+ `janus.magicLink.sent` event: `magicLink.request` answered `null`, for the
106
+ same reasons.
107
+
108
+ ### `janus.signIn.refused` with `TOKEN_SPENT` and `janus.signIn.magicLink`, for users who clicked once
109
+
110
+ **When:** sign-in links answer `TOKEN_SPENT` on the first click the user
111
+ remembers, often for the users of one company's mail.
112
+
113
+ **Why:** a mail scanner — a gateway, a webmail's link check, an antivirus —
114
+ opened the link before the user, and the route that answers the link's
115
+ `GET` calls `magicLink.confirm`. The scanner's visit spent the token. The
116
+ span of that `GET` is the first `janus.magicLink.confirm` of the token, and
117
+ the user's click the second.
118
+
119
+ **Fix:** make the link's `GET` a page that spends nothing, and confirm from
120
+ its button's `POST`; see
121
+ [confirming from a `POST`](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/magic-link.md#confirming-from-a-post-never-from-the-links-get).
122
+
103
123
  ### Every wrong password fails its span
104
124
 
105
125
  **When:** `janus.signIn` spans fail with `janus.error.code` absent, for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus-telemetry",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Observability for @nxgt/janus on @nxgt/telemetry: a span per flow and per permission check, and the security events worth an audit trail — never a login, an e-mail, a password, a session token, a one-time token or a session id",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,13 +49,13 @@
49
49
  ]
50
50
  },
51
51
  "devDependencies": {
52
- "@nxgt/janus": "^0.14.0",
52
+ "@nxgt/janus": "^0.16.0",
53
53
  "@nxgt/telemetry": "0.2.1",
54
54
  "@types/bun": "^1.4.2",
55
55
  "zod": "^4.6.5"
56
56
  },
57
57
  "peerDependencies": {
58
- "@nxgt/janus": "^0.14.0",
58
+ "@nxgt/janus": "^0.16.0",
59
59
  "@nxgt/telemetry": ">=0.2.1 <1",
60
60
  "typescript": "^6.0.3"
61
61
  }