@nxgt/janus-telemetry 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,8 +3,9 @@
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, why a sign-in was refused, who was asked for a second
7
- factor and who enrolled, activated or disabled one, which users were deleted
6
+ signed up, in and out, who was sent a sign-in code, why a sign-in was
7
+ refused, who was asked for a second factor and who enrolled, activated or
8
+ disabled one, which users were deleted
8
9
  or deactivated, whose password changed, and who was granted what. **Never a
9
10
  login, an e-mail, a password, a session token, a one-time token, a
10
11
  challenge, a code, a TOTP secret or a session id.**
@@ -32,7 +33,7 @@ bun add @nxgt/janus-telemetry @nxgt/janus @nxgt/telemetry
32
33
  bun add -d typescript
33
34
  ```
34
35
 
35
- Every peer is required: `@nxgt/janus` (0.5, the release with the second factor), `@nxgt/telemetry` (0.2.1 or later) and
36
+ Every peer is required: `@nxgt/janus` (0.6, the release with sign-in codes), `@nxgt/telemetry` (0.2.1 or later) and
36
37
  `typescript` (6). `@nxgt/janus` and `@nxgt/telemetry` are **peers**: one copy of `@nxgt/janus`, so
37
38
  `instanceof JanusError` holds, and one of `@nxgt/telemetry`, so there is one
38
39
  current span. Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`.
@@ -51,7 +52,7 @@ current span. Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`.
51
52
 
52
53
  | Span | Attributes |
53
54
  | --- | --- |
54
- | `janus.<flow>`, `janus.<type>.<flow>` — `janus.secondFactor.confirm` included | `janus.user.type`; `user.id` once the answer names a user; `janus.session.renewed` for `authenticate`; `janus.signIn.status` for `signIn` — `signedIn`, or `secondFactor` when it answered a challenge |
55
+ | `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 |
55
56
  | `janus.can` | `janus.subject.type`, `janus.subject.id`, `janus.permission`, `janus.object.type`, `janus.object.id`, and the answer, `janus.allowed` |
56
57
  | `janus.list` | the subject, `janus.permission`, `janus.object.type`, and `janus.page.items`, how many it found |
57
58
  | `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 |
@@ -71,9 +72,10 @@ when the flow knows them — `janus.signOut` carries neither:
71
72
  | Event | Severity | When |
72
73
  | --- | --- | --- |
73
74
  | `janus.signUp` | info | a user signed up |
74
- | `janus.signIn` | info | a user signed in — by `signIn`, or by `secondFactor.confirm`, which adds `janus.signIn.secondFactor: true` |
75
- | `janus.signIn.secondFactor` | info | the password was right and a code was asked for: `signIn` answered a challenge. `janus.user.type` only — the answer names no user; the `janus.signIn` of the `confirm` that follows does |
76
- | `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`: `CODE_INVALID` with `user.id` and `janus.secondFactor.attemptsLeft`, a `TOKEN_*` code, `SECOND_FACTOR_NOT_ENROLLED`, or `VERSION_CONFLICT` for one code sent twice at once |
75
+ | `janus.signInCode.sent` | info | `signInCode.request` issued a code, with the `user.id` it is for. A request that answered `null` writes nothing |
76
+ | `janus.signIn` | info | a user signed in — by `signIn`; by `signInCode.confirm`, which adds `janus.signIn.code: true`; or by `secondFactor.confirm`, which adds `janus.signIn.secondFactor: true` |
77
+ | `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` only — the answer names no user; the `janus.signIn` of the `confirm` that follows does |
78
+ | `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` |
77
79
  | `janus.secondFactor.enrolled`, `janus.secondFactor.activated`, `janus.secondFactor.disabled` | info | `enroll`, `activate` and `disable`, with the `user.id` they were called for |
78
80
  | `janus.signOut` | info | a session was signed out |
79
81
  | `janus.signOutEverywhere` | info | every session of a user was revoked |
@@ -96,20 +98,26 @@ when the flow knows them — `janus.signOut` carries neither:
96
98
  - **A refused sign-in for bad credentials names no user.** `janus.signIn.refused`
97
99
  with `CREDENTIALS_INVALID` carries the reason and the user type, never the
98
100
  login that was tried: rate limiting per login is the application's, from the
99
- request. Only a refusal after the password was right carries `user.id`:
100
- `USER_INACTIVE`, and a second factor's `CODE_INVALID` or
101
- `SECOND_FACTOR_NOT_ENROLLED`.
102
- - **A wrong second-factor code is a warning, not a failure.** `CODE_INVALID`
103
- leaves the `janus.secondFactor.confirm` span `ok`, and writes
104
- `janus.signIn.refused` with `janus.secondFactor.attemptsLeft`: alert on a
105
- user whose count reaches `0` again and again, not on the span.
101
+ request. Only a refusal after the password was right, or against a
102
+ challenge, carries `user.id`: `USER_INACTIVE`, a `CODE_INVALID`,
103
+ `TOKEN_STALE` or `SECOND_FACTOR_NOT_ENROLLED`. Nor does
104
+ `janus.signInCode.sent` name the address: a request for nobody writes
105
+ nothing at all.
106
+ - **A wrong code is a warning, not a failure.** `CODE_INVALID` leaves the
107
+ `janus.secondFactor.confirm` or `janus.signInCode.confirm` span `ok`, and
108
+ writes `janus.signIn.refused` with `janus.secondFactor.attemptsLeft` — named
109
+ so for an e-mailed code too: alert on a user whose count reaches `0` again
110
+ and again, not on the span. **Every** refusal of `signInCode.confirm`
111
+ carries `janus.signIn.code: true` — `CODE_INVALID`, a `TOKEN_*` code
112
+ including `TOKEN_STALE`, `USER_INACTIVE` and `VERSION_CONFLICT` alike — so
113
+ a filter on the mark sees the whole e-mailed-code flow.
106
114
  - **One copy of `@nxgt/janus`.** A refusal is told from a failure by
107
115
  `instanceof JanusError`: with a second copy installed, every wrong password
108
116
  fails its span.
109
117
 
110
118
  ## Documentation
111
119
 
112
- - [Tracing Janus](docs/guide/tracing.md) — wiring it with a Hono app, reading the spans and events, the audit trail, a sign-in with a second factor
120
+ - [Tracing Janus](docs/guide/tracing.md) — wiring it with a Hono app, reading the spans and events, the audit trail, a sign-in with a second factor or an e-mailed code
113
121
  - [Troubleshooting](docs/troubleshooting.md) — what you see, why, and the fix
114
122
  - [Roadmap](docs/roadmap.md) — what is next, and what is not planned
115
123
 
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../src/flows.ts"],"names":[],"mappings":"AAiCA,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAuJD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAQ/D"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../src/flows.ts"],"names":[],"mappings":"AAkCA,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAwLD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAQ/D"}
package/dist/index.js CHANGED
@@ -94,6 +94,7 @@ var events = {
94
94
  signedIn: event("janus.signIn"),
95
95
  signInRefused: event("janus.signIn.refused"),
96
96
  secondFactorAsked: event("janus.signIn.secondFactor"),
97
+ signInCodeSent: event("janus.signInCode.sent"),
97
98
  secondFactorEnrolled: event("janus.secondFactor.enrolled"),
98
99
  secondFactorActivated: event("janus.secondFactor.activated"),
99
100
  secondFactorDisabled: event("janus.secondFactor.disabled"),
@@ -121,6 +122,26 @@ var WRITTEN = {
121
122
  log.info(events.signedIn(userFields(call, outcome.value)));
122
123
  }
123
124
  },
125
+ "signInCode.request": (call, outcome) => {
126
+ if (outcome.ok && outcome.value !== null) {
127
+ log.info(events.signInCodeSent(userFields(call, outcome.value)));
128
+ }
129
+ },
130
+ "signInCode.confirm": (call, outcome) => {
131
+ if (!outcome.ok) {
132
+ log.warn(events.signInRefused({
133
+ ...refusalFields(call, outcome.refusal),
134
+ "janus.signIn.code": true
135
+ }));
136
+ } else if (statusOf(outcome.value) === "secondFactor") {
137
+ log.info(events.secondFactorAsked(fieldsOf({ "janus.user.type": call.userType })));
138
+ } else {
139
+ log.info(events.signedIn({
140
+ ...userFields(call, outcome.value),
141
+ "janus.signIn.code": true
142
+ }));
143
+ }
144
+ },
124
145
  "secondFactor.confirm": (call, outcome) => {
125
146
  if (outcome.ok) {
126
147
  log.info(events.signedIn({
@@ -213,7 +234,7 @@ function answered(scope, call, outcome) {
213
234
  if (typeof value === "object" && value !== null && "renewed" in value) {
214
235
  scope.attribute("janus.session.renewed", value.renewed === true);
215
236
  }
216
- const status = call.flow === "signIn" ? statusOf(value) : undefined;
237
+ const status = call.flow === "signIn" || call.flow === "signInCode.confirm" ? statusOf(value) : undefined;
217
238
  if (status !== undefined)
218
239
  scope.attribute("janus.signIn.status", status);
219
240
  }
@@ -324,5 +345,5 @@ export {
324
345
  instrumentPermissions
325
346
  };
326
347
 
327
- //# debugId=1F91222C283742CE64756E2164756E21
348
+ //# debugId=89F3B2CDD858725264756E2164756E21
328
349
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,11 +2,11 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/flows.ts", "../src/traced.ts", "../src/permissions.ts"],
4
4
  "sourcesContent": [
5
- "import type { JanusError } from '@nxgt/janus';\nimport { createLogger, event, type SpanScope } from '@nxgt/telemetry';\nimport {\n\ttype Fields,\n\tfieldsOf,\n\tidOf,\n\ttype Outcome,\n\ttraced,\n\ttypeOf,\n} from './traced';\n\nconst log = createLogger('@nxgt/janus');\n\n/** The events a security review reads: who signed in, out, up, and what changed. */\nconst events = {\n\tsignedUp: event('janus.signUp'),\n\tsignedIn: event('janus.signIn'),\n\tsignInRefused: event('janus.signIn.refused'),\n\tsecondFactorAsked: event('janus.signIn.secondFactor'),\n\tsecondFactorEnrolled: event('janus.secondFactor.enrolled'),\n\tsecondFactorActivated: event('janus.secondFactor.activated'),\n\tsecondFactorDisabled: event('janus.secondFactor.disabled'),\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/** 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/** One call of a flow, as the events read it. */\ninterface Call {\n\t/** `signIn`, `verifyEmail.confirm`: the flow without the user type. */\n\treadonly flow: string;\n\treadonly userType: string | undefined;\n\treadonly args: readonly unknown[];\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 */\nconst 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) {\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(\n\t\t\t\t\tfieldsOf({ 'janus.user.type': call.userType }),\n\t\t\t\t),\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'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.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\n/** The user an answer names — `{ user }` or the user itself — as fields. */\nfunction 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/** The user a flow was called for: its first argument. */\nfunction 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 */\nfunction 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. */\nfunction 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/** 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\tconst fields = 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 = call.flow === 'signIn' ? 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/** `cookie` answers synchronously: it is the one part of `janus()` not traced. */\nconst UNTRACED = new Set(['cookie']);\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,\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\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 */\nfunction 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",
5
+ "import type { JanusError } from '@nxgt/janus';\nimport { createLogger, event, type SpanScope } from '@nxgt/telemetry';\nimport {\n\ttype Fields,\n\tfieldsOf,\n\tidOf,\n\ttype Outcome,\n\ttraced,\n\ttypeOf,\n} from './traced';\n\nconst log = createLogger('@nxgt/janus');\n\n/** The events a security review reads: who signed in, out, up, and what changed. */\nconst events = {\n\tsignedUp: event('janus.signUp'),\n\tsignedIn: event('janus.signIn'),\n\tsignInRefused: event('janus.signIn.refused'),\n\tsecondFactorAsked: event('janus.signIn.secondFactor'),\n\tsignInCodeSent: event('janus.signInCode.sent'),\n\tsecondFactorEnrolled: event('janus.secondFactor.enrolled'),\n\tsecondFactorActivated: event('janus.secondFactor.activated'),\n\tsecondFactorDisabled: event('janus.secondFactor.disabled'),\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/** 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/** One call of a flow, as the events read it. */\ninterface Call {\n\t/** `signIn`, `verifyEmail.confirm`: the flow without the user type. */\n\treadonly flow: string;\n\treadonly userType: string | undefined;\n\treadonly args: readonly unknown[];\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 */\nconst 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) {\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(\n\t\t\t\t\tfieldsOf({ 'janus.user.type': call.userType }),\n\t\t\t\t),\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(\n\t\t\t\t\tfieldsOf({ 'janus.user.type': call.userType }),\n\t\t\t\t),\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'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.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\n/** The user an answer names — `{ user }` or the user itself — as fields. */\nfunction 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/** The user a flow was called for: its first argument. */\nfunction 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 */\nfunction 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. */\nfunction 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/** 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\tconst fields = 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/**\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,\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\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 */\nfunction 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",
6
6
  "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 '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",
7
7
  "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"
8
8
  ],
9
- "mappings": ";AACA;;;ACDA;AACA;AAQA,SAAS,MAAM,CAAC,MAA6C;AAAA,EAC5D,QAAQ;AAAA,SACF;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,MACJ,OAAO;AAAA,SACH;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,SACA;AAAA,MACJ,OAAO;AAAA;AAAA;AAKH,SAAS,SAAS,CAAC,OAAqC;AAAA,EAC9D,OAAO,iBAAiB,cAAc,OAAO,MAAM,IAAI,MAAM;AAAA;AAOvD,SAAS,QAAQ,CACvB,QACS;AAAA,EACT,MAAM,OAAkD,CAAC;AAAA,EACzD,YAAY,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;AAAA,IAClD,IAAI,UAAU;AAAA,MAAW,KAAK,OAAO;AAAA,EACtC;AAAA,EACA,OAAO;AAAA;AAID,SAAS,IAAI,CAAC,OAAoC;AAAA,EACxD,IAAI,OAAO,UAAU;AAAA,IAAU,OAAO;AAAA,EACtC,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,QAAQ,OAAO;AAAA,IACjE,MAAM,KAAc,MAAM;AAAA,IAC1B,IAAI,OAAO,OAAO;AAAA,MAAU,OAAO;AAAA,EACpC;AAAA,EACA;AAAA;AAIM,SAAS,MAAM,CAAC,OAAoC;AAAA,EAC1D,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,OAAO;AAAA,IACnE,MAAM,OAAgB,MAAM;AAAA,IAC5B,IAAI,OAAO,SAAS;AAAA,MAAU,OAAO;AAAA,EACtC;AAAA,EACA;AAAA;AAiBD,eAAsB,MAAS,CAC9B,MACA,QACA,MACA,OACa;AAAA,EACb,MAAM,QAAQ,MAAM,KAAK,MAAM,EAAE,YAAY,OAAO,GAAG,OAAO,UAAU;AAAA,IACvE,IAAI;AAAA,IACJ,IAAI;AAAA,MACH,UAAU,EAAE,IAAI,MAAM,OAAO,MAAM,KAAK,EAAE;AAAA,MACzC,OAAO,OAAO;AAAA,MACf,IAAI,iBAAiB,YAAY;AAAA,QAChC,MAAM,UAAU,oBAAoB,MAAM,IAAI;AAAA,QAC9C,IAAI,MAAM,SAAS;AAAA,UAClB,MAAM,UAAU,oBAAoB,MAAM,IAAI;AAAA,QAC/C,IAAI,MAAM,cAAc,WAAW;AAAA,UAClC,MAAM,UAAU,yBAAyB,MAAM,SAAS;AAAA,QACzD;AAAA,MACD;AAAA,MACA,IAAI,CAAC,UAAU,KAAK;AAAA,QAAG,MAAM;AAAA,MAC7B,MAAM,UAAU,iBAAiB,MAAM,IAAI;AAAA,MAC3C,UAAU,EAAE,IAAI,OAAO,SAAS,MAAM;AAAA;AAAA,IAEvC,IAAI;AAAA,MACH,MAAM,OAAO,OAAO;AAAA,MACnB,MAAM;AAAA,IAGR,OAAO;AAAA,GACP;AAAA,EACD,IAAI,CAAC,MAAM;AAAA,IAAI,MAAM,MAAM;AAAA,EAC3B,OAAO,MAAM;AAAA;;;AD3Gd,IAAM,MAAM,aAAa,aAAa;AAGtC,IAAM,SAAS;AAAA,EACd,UAAU,MAAM,cAAc;AAAA,EAC9B,UAAU,MAAM,cAAc;AAAA,EAC9B,eAAe,MAAM,sBAAsB;AAAA,EAC3C,mBAAmB,MAAM,2BAA2B;AAAA,EACpD,sBAAsB,MAAM,6BAA6B;AAAA,EACzD,uBAAuB,MAAM,8BAA8B;AAAA,EAC3D,sBAAsB,MAAM,6BAA6B;AAAA,EACzD,WAAW,MAAM,eAAe;AAAA,EAChC,qBAAqB,MAAM,yBAAyB;AAAA,EACpD,aAAa,MAAM,oBAAoB;AAAA,EACvC,eAAe,MAAM,sBAAsB;AAAA,EAC3C,iBAAiB,MAAM,wBAAwB;AAAA,EAC/C,iBAAiB,MAAM,wBAAwB;AAAA,EAC/C,aAAa,MAAM,oBAAoB;AAAA,EACvC,eAAe,MAAM,sBAAsB;AAAA,EAC3C,eAAe,MAAM,sBAAsB;AAC5C;AAoBA,IAAM,UAEF;AAAA,EACH,QAAQ,CAAC,MAAM,YAAY;AAAA,IAC1B,IAAI,QAAQ;AAAA,MAAI,IAAI,KAAK,OAAO,SAAS,WAAW,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA;AAAA,EAE1E,QAAQ,CAAC,MAAM,YAAY;AAAA,IAC1B,IAAI,CAAC,QAAQ,IAAI;AAAA,MAChB,IAAI,KAAK,OAAO,cAAc,cAAc,MAAM,QAAQ,OAAO,CAAC,CAAC;AAAA,IACpE,EAAO,SAAI,SAAS,QAAQ,KAAK,MAAM,gBAAgB;AAAA,MAEtD,IAAI,KACH,OAAO,kBACN,SAAS,EAAE,mBAAmB,KAAK,SAAS,CAAC,CAC9C,CACD;AAAA,IACD,EAAO;AAAA,MACN,IAAI,KAAK,OAAO,SAAS,WAAW,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA;AAAA;AAAA,EAG3D,wBAAwB,CAAC,MAAM,YAAY;AAAA,IAC1C,IAAI,QAAQ,IAAI;AAAA,MACf,IAAI,KACH,OAAO,SAAS;AAAA,WACZ,WAAW,MAAM,QAAQ,KAAK;AAAA,QACjC,6BAA6B;AAAA,MAC9B,CAAC,CACF;AAAA,IACD,EAAO;AAAA,MACN,IAAI,KAAK,OAAO,cAAc,cAAc,MAAM,QAAQ,OAAO,CAAC,CAAC;AAAA;AAAA;AAAA,EAGrE,uBAAuB,CAAC,MAAM,YAAY;AAAA,IACzC,IAAI,QAAQ;AAAA,MAAI,IAAI,KAAK,OAAO,qBAAqB,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAEzE,yBAAyB,CAAC,MAAM,YAAY;AAAA,IAC3C,IAAI,QAAQ;AAAA,MAAI,IAAI,KAAK,OAAO,sBAAsB,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAE1E,wBAAwB,CAAC,MAAM,YAAY;AAAA,IAC1C,IAAI,QAAQ;AAAA,MAAI,IAAI,KAAK,OAAO,qBAAqB,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAEzE,SAAS,CAAC,OAAO,YAAY;AAAA,IAC5B,IAAI,QAAQ,MAAM,QAAQ,UAAU;AAAA,MAAM,IAAI,KAAK,OAAO,UAAU,CAAC;AAAA;AAAA,EAEtE,mBAAmB,CAAC,MAAM,YAAY;AAAA,IACrC,IAAI,QAAQ;AAAA,MAAI,IAAI,KAAK,OAAO,oBAAoB,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAExE,QAAQ,CAAC,MAAM,YAAY;AAAA,IAC1B,IAAI,QAAQ,MAAM,QAAQ,UAAU,MAAM;AAAA,MACzC,IAAI,KAAK,OAAO,YAAY,aAAa,IAAI,CAAC,CAAC;AAAA,IAChD;AAAA;AAAA,EAED,WAAW,CAAC,MAAM,YAAY;AAAA,IAC7B,IAAI,CAAC,QAAQ;AAAA,MAAI;AAAA,IACjB,MAAM,UACL,KAAK,KAAK,OAAO,OAAO,OAAO,gBAAgB,OAAO;AAAA,IACvD,IAAI,KAAK,QAAQ,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAErC,gBAAgB,CAAC,MAAM,YAAY;AAAA,IAClC,IAAI,QAAQ;AAAA,MAAI,IAAI,KAAK,OAAO,gBAAgB,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAEpE,aAAa,CAAC,MAAM,YAAY;AAAA,IAC/B,IAAI,QAAQ;AAAA,MAAI,IAAI,KAAK,OAAO,YAAY,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAEhE,yBAAyB,CAAC,MAAM,YAAY;AAAA,IAC3C,IAAI,QAAQ;AAAA,MACX,IAAI,KAAK,OAAO,cAAc,WAAW,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA;AAAA,EAEhE,uBAAuB,CAAC,MAAM,YAAY;AAAA,IACzC,IAAI,QAAQ;AAAA,MACX,IAAI,KAAK,OAAO,cAAc,WAAW,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA;AAEjE;AAGA,SAAS,UAAU,CAAC,MAAY,OAAwB;AAAA,EACvD,MAAM,OACL,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,QACtD,MAAM,OACN;AAAA,EACJ,OAAO,SAAS;AAAA,IACf,mBAAmB,OAAO,IAAI,KAAK,KAAK;AAAA,IACxC,WAAW,KAAK,IAAI;AAAA,EACrB,CAAC;AAAA;AAIF,SAAS,YAAY,CAAC,MAAoB;AAAA,EACzC,OAAO,SAAS;AAAA,IACf,mBAAmB,KAAK;AAAA,IACxB,WAAW,KAAK,KAAK,KAAK,EAAE;AAAA,EAC7B,CAAC;AAAA;AAOF,SAAS,aAAa,CAAC,MAAY,SAA6B;AAAA,EAC/D,OAAO,SAAS;AAAA,IACf,mBAAmB,QAAQ,YAAY,KAAK;AAAA,IAC5C,iBAAiB,QAAQ;AAAA,IACzB,wBAAwB,QAAQ;AAAA,IAChC,mCAAmC,QAAQ;AAAA,IAC3C,WAAW,QAAQ;AAAA,EACpB,CAAC;AAAA;AAIF,SAAS,QAAQ,CAAC,OAAoC;AAAA,EACrD,OAAO,OAAO,UAAU,YACvB,UAAU,QACV,YAAY,SACZ,OAAO,MAAM,WAAW,WACtB,MAAM,SACN;AAAA;AAIJ,SAAS,QAAQ,CAAC,OAAkB,MAAY,SAAwB;AAAA,EACvE,IAAI,QAAQ,IAAI;AAAA,IACf,MAAM,SAAS,WAAW,MAAM,QAAQ,KAAK;AAAA,IAC7C,YAAY,KAAK,UAAU,OAAO,QAAQ,MAAM;AAAA,MAC/C,MAAM,UAAU,KAAK,KAAK;AAAA,IAC3B,MAAM,QAAQ,QAAQ;AAAA,IACtB,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa,OAAO;AAAA,MACtE,MAAM,UAAU,yBAAyB,MAAM,YAAY,IAAI;AAAA,IAChE;AAAA,IACA,MAAM,SAAS,KAAK,SAAS,WAAW,SAAS,KAAK,IAAI;AAAA,IAC1D,IAAI,WAAW;AAAA,MAAW,MAAM,UAAU,uBAAuB,MAAM;AAAA,EACxE;AAAA,EACA,QAAQ,KAAK,QAAQ,MAAM,OAAO;AAAA;AAInC,IAAM,WAAW,IAAI,IAAI,CAAC,QAAQ,CAAC;AAqB5B,SAAS,eAAoC,CAAC,MAAY;AAAA,EAChE,MAAM,QAAQ,KAAK,MAAM,OAAO,CAAC,SAAS,OAAO,SAAS,QAAQ;AAAA,EAClE,MAAM,OAAO,MAAM,WAAW,IAAI,MAAM,KAAK;AAAA,EAC7C,OAAO,YAAY,MAAM,CAAC,GAAG,CAAC,SAC7B,KAAK,OAAO,aAAa,MAAM,SAAS,KAAK,EAAE,IAC5C,EAAE,UAAU,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC,EAAE,IACzC,EAAE,UAAU,MAAM,MAAM,KAAK,CACjC;AAAA;AAaD,SAAS,WAA6B,CACrC,QACA,MACA,OACI;AAAA,EACJ,MAAM,OAAgC,CAAC;AAAA,EACvC,YAAY,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;AAAA,IAClD,IAAI,SAAS,IAAI,GAAG,GAAG;AAAA,MACtB,KAAK,OAAO;AAAA,IACb,EAAO,SAAI,OAAO,UAAU,YAAY;AAAA,MACvC,KAAK,OAAO,cACX,OACA,QACA,CAAC,GAAG,MAAM,GAAG,GACb,KACD;AAAA,IACD,EAAO,SACN,OAAO,UAAU,YACjB,UAAU,QACV,CAAC,MAAM,QAAQ,KAAK,GACnB;AAAA,MACD,KAAK,OAAO,YAAY,OAAO,CAAC,GAAG,MAAM,GAAG,GAAG,KAAK;AAAA,IACrD,EAAO;AAAA,MACN,KAAK,OAAO;AAAA;AAAA,EAEd;AAAA,EACA,OAAO,OAAO,OAAO,IAAI;AAAA;AAG1B,SAAS,aAAa,CACrB,IACA,MACA,MACA,OAC2C;AAAA,EAC3C,QAAQ,UAAU,SAAS,MAAM,IAAI;AAAA,EACrC,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;AAAA,EACnC,MAAM,SAAS,SAAS,EAAE,mBAAmB,SAAS,CAAC;AAAA,EACvD,OAAO,IAAI,SAAS;AAAA,IACnB,MAAM,OAAa,EAAE,MAAM,KAAK,KAAK,GAAG,GAAG,UAAU,KAAK;AAAA,IAC1D,OAAO,OACN,MACA,QACA,MAAM,QAAQ,QAAQ,GAAG,MAAM,MAAM,IAAI,CAAC,GAC1C,CAAC,OAAO,YAAY,SAAS,OAAO,MAAM,OAAO,CAClD;AAAA;AAAA;;AEhRF;AACA,yBAAS,wBAAc;AAGvB,IAAM,OAAM,cAAa,aAAa;AAGtC,IAAM,UAAU,OAAM,qBAAqB;AAC3C,IAAM,UAAU,OAAM,qBAAqB;AAO3C,SAAS,aAAa,CACrB,SACA,WACS;AAAA,EACT,MAAM,OAAO,OAAO,OAAO;AAAA,EAC3B,MAAM,WACL,OAAO,YAAY,YAAY,YAAY,QAAQ,cAAc,UAC9D,QAAQ,WACR;AAAA,EACJ,MAAM,QACL,OAAO,aAAa,aACnB,QAAQ,OAAO,KAAK,SAAS,aAAa,CAAC,UAAU,IAAI,IAAI;AAAA,EAC/D,OAAO,SAAS;AAAA,IACf,sBAAsB;AAAA,IACtB,oBAAoB,KAAK,OAAO;AAAA,IAChC,0BAA0B,QAAQ,WAAW;AAAA,EAC9C,CAAC;AAAA;AAGF,SAAS,YAAY,CAAC,QAAyB;AAAA,EAC9C,OAAO,SAAS;AAAA,IACf,qBAAqB,OAAO,MAAM;AAAA,IAClC,mBAAmB,KAAK,MAAM;AAAA,EAC/B,CAAC;AAAA;AAgBF,IAAM,aAAa,CAClB,eACuD;AAAA,EACvD,KACC,CAAC,OACD,IAAI,SACH,OACC,aACA,SAAS;AAAA,OACL,cAAc,KAAK,IAAI,SAAS;AAAA,IACnC,oBAAoB,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAAA,OACzD,aAAa,KAAK,EAAE;AAAA,EACxB,CAAC,GACD,MAAM,GAAG,GAAG,IAAI,GAChB,CAAC,OAAO,YAAY;AAAA,IACnB,IAAI,QAAQ;AAAA,MACX,MAAM,UAAU,iBAAiB,QAAQ,UAAU,IAAI;AAAA,GAE1D;AAAA,EACF,MACC,CAAC,OACD,IAAI,SACH,OACC,cACA,SAAS;AAAA,OACL,cAAc,KAAK,IAAI,SAAS;AAAA,IACnC,oBAAoB,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAAA,IAC5D,qBACC,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAAA,EAC1C,CAAC,GACD,MAAM,GAAG,GAAG,IAAI,GAChB,CAAC,OAAO,YAAY;AAAA,IACnB,MAAM,OAAO,QAAQ,KAAK,QAAQ,QAAQ;AAAA,IAC1C,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,WAAW,MAAM;AAAA,MACjE,MAAM,QAAQ,KAAK;AAAA,MACnB,IAAI,MAAM,QAAQ,KAAK;AAAA,QACtB,MAAM,UAAU,oBAAoB,MAAM,MAAM;AAAA,IAClD;AAAA,GAEF;AAAA,EACF,OAAO,CAAC,OAAO,WAAW,eAAe,SAAS,IAAI,SAAS;AAAA,EAC/D,QAAQ,CAAC,OAAO,WAAW,gBAAgB,SAAS,IAAI,SAAS;AAClE;AAEA,SAAS,UAAU,CAClB,MACA,SACA,IACA,WACS;AAAA,EACT,OAAO,IAAI,SAAS;AAAA,IACnB,MAAM,SAAS,SAAS;AAAA,SACpB,aAAa,KAAK,EAAE;AAAA,MACvB,kBAAkB,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAAA,SACvD,cAAc,KAAK,IAAI,SAAS;AAAA,IACpC,CAAC;AAAA,IACD,OAAO,OACN,MACA,QACA,MAAM,GAAG,GAAG,IAAI,GAChB,CAAC,QAAQ,YAAY;AAAA,MACpB,IAAI,QAAQ;AAAA,QAAI,KAAI,KAAK,QAAQ,MAAM,CAAC;AAAA,KAE1C;AAAA;AAAA;AAiBK,SAAS,qBAAgD,CAAC,QAAc;AAAA,EAG9E,MAAM,OAAgC,OAAO,YAC5C,OAAO,QAAQ,MAAM,CACtB;AAAA,EACA,MAAM,YAAY,IAAI,IAAI,OAAO,OAAO,YAAY,CAAC,CAAC;AAAA,EACtD,YAAY,KAAK,WAAW,OAAO,QAAQ,WAAW,SAAS,CAAC,GAAG;AAAA,IAClE,MAAM,SAAkB,KAAK;AAAA,IAC7B,IAAI,OAAO,WAAW;AAAA,MAAY;AAAA,IAClC,KAAK,OAAO,OAAO,IAAI,SACtB,QAAQ,QAAQ,OAAO,MAAM,QAAQ,IAAI,CAAC,CAC3C;AAAA,EACD;AAAA,EACA,OAAO,OAAO,OAAO,IAAI;AAAA;",
10
- "debugId": "1F91222C283742CE64756E2164756E21",
9
+ "mappings": "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",
10
+ "debugId": "89F3B2CDD858725264756E2164756E21",
11
11
  "names": []
12
12
  }
package/docs/README.md CHANGED
@@ -5,7 +5,7 @@ detail.
5
5
 
6
6
  | Page | Read it when |
7
7
  | --- | --- |
8
- | [Tracing Janus](guide/tracing.md) | You are wiring it into an application, reading what it wrote, or following a sign-in with a second factor |
8
+ | [Tracing Janus](guide/tracing.md) | You are wiring it into an application, reading what it wrote, or following a sign-in with a second factor or an e-mailed code |
9
9
  | [Troubleshooting](troubleshooting.md) | A span or an event is missing, or says something you did not expect |
10
10
  | [Roadmap](roadmap.md) | You want to know what is coming, what shipped, and what is deliberately not planned |
11
11
 
@@ -131,18 +131,81 @@ with the `user.id` they were called for — `janus.secondFactor.enrolled`,
131
131
  (`SECOND_FACTOR_ACTIVE`, a wrong first code) writes no event: its span
132
132
  carries `janus.refusal`, like any refusal.
133
133
 
134
+ ## A code sent by e-mail
135
+
136
+ A sign-in by e-mailed code is two calls too — `signInCode.request`, then
137
+ `signInCode.confirm` — and each writes one event:
138
+
139
+ ```
140
+ POST /sign-in/email server
141
+ └─ janus.signInCode.request janus.user.type=user user.id=0199…
142
+ log janus.signInCode.sent janus.user.type=user user.id=0199…
143
+
144
+ POST /sign-in/email/code server
145
+ └─ janus.signInCode.confirm janus.user.type=user user.id=0199… janus.signIn.status=signedIn
146
+ log janus.signIn user.id=0199… janus.signIn.code=true
147
+ ```
148
+
149
+ `janus.signInCode.sent` is written only when a code was issued. A request
150
+ for an address nobody holds — or an inactive user's — answers `null`, and
151
+ its span carries `janus.user.type` alone, with no event: the address that
152
+ was typed is never written, so the trail cannot be read back as a list of
153
+ who has an account. `janus.signIn.code: true` tells a sign-in by code from
154
+ one by password.
155
+
156
+ The `confirm` span carries `janus.signIn.status`, as `signIn`'s does. For a
157
+ user whose second factor is active, the code opens no session: the status
158
+ is `secondFactor`, the event is `janus.signIn.secondFactor`, and the
159
+ `janus.signIn` comes from the `secondFactor.confirm` that follows, with
160
+ `janus.signIn.secondFactor: true` and no `janus.signIn.code`:
161
+
162
+ ```
163
+ POST /sign-in/email/code server
164
+ └─ janus.signInCode.confirm janus.user.type=user janus.signIn.status=secondFactor
165
+ log janus.signIn.secondFactor janus.user.type=user
166
+ ```
167
+
168
+ A refused code is the same warning as a second factor's — the attribute
169
+ keeps its name, `janus.secondFactor.attemptsLeft`, for both — marked with
170
+ `janus.signIn.code: true` so an alert can tell the two apart:
171
+
172
+ ```
173
+ janus.signIn.refused janus.refusal=CODE_INVALID janus.secondFactor.attemptsLeft=4 janus.signIn.code=true user.id=0199…
174
+ ```
175
+
176
+ A lapsed, spent or unknown challenge is a `janus.signIn.refused` with its
177
+ `TOKEN_*` code and no `user.id`; `TOKEN_STALE` — the e-mail changed since
178
+ the code was sent — and `USER_INACTIVE` name the user. **Every** refusal of
179
+ `signInCode.confirm` carries `janus.signIn.code: true`, not only
180
+ `CODE_INVALID`: the `TOKEN_*` codes, `TOKEN_STALE`, `USER_INACTIVE` and
181
+ `VERSION_CONFLICT` too. A filter on the mark sees every refused e-mailed
182
+ code, and nothing of the second factor:
183
+
184
+ ```
185
+ janus.signIn.refused janus.refusal=TOKEN_EXPIRED janus.signIn.code=true
186
+ ```
187
+
188
+ Two signals worth an alert: a user whose codes run out of attempts, as for a
189
+ second factor, and a user sent codes again and again — someone filling an
190
+ inbox, or trying their luck with a challenge at a time:
191
+
192
+ ```ts
193
+ // name = 'janus.signInCode.sent', grouped by user.id, more than 10 in an hour
194
+ ```
195
+
134
196
  ## What is never written
135
197
 
136
198
  A login, an e-mail, a password, a session token, a one-time token, a
137
- second factor's challenge, a code or its TOTP secret, the
138
- `otpauth://` URI that holds it, a session id: nothing a log reader could
199
+ challenge — a second factor's or a sign-in code's — a code, a TOTP secret,
200
+ the `otpauth://` URI that holds it, a session id: nothing a log reader could
139
201
  sign in with, or use to tell who holds an account. A refused sign-in by
140
202
  an unknown login says `janus.refusal.reason: 'unknownLogin'`, not which login
141
- was tried; only a refusal after the password was right — `USER_INACTIVE`, a second
142
- factor's `CODE_INVALID` or `SECOND_FACTOR_NOT_ENROLLED` — names the user.
143
- The spec that holds this runs every flow — `enroll`, `activate`, a refused
144
- and an accepted `confirm` among them — and searches every signal for each of
145
- them.
203
+ was tried; only a refusal after the password was right, or against a
204
+ challenge — `USER_INACTIVE`, `CODE_INVALID`, `TOKEN_STALE` or
205
+ `SECOND_FACTOR_NOT_ENROLLED` — names the user. The specs that hold this run
206
+ every flow — `enroll`, `activate`, a sign-in code requested for nobody and
207
+ for a user, a refused and an accepted `confirm` among them — and search
208
+ every signal for each code, challenge and session token they handled.
146
209
 
147
210
  ## In a test
148
211
 
package/docs/roadmap.md CHANGED
@@ -23,8 +23,9 @@ Nothing yet.
23
23
 
24
24
  - **An OpenTelemetry SDK** — like `@nxgt/telemetry`, it speaks OTLP through its
25
25
  exporters and depends on no `@opentelemetry/*` package.
26
- - **Writing a login, an e-mail, a password, a session token, a one-time token
27
- or a session id** — in any span or event, on any setting.
26
+ - **Writing a login, an e-mail, a password, a session token, a one-time token,
27
+ a one-time code, a challenge or a session id** — in any span or event, on
28
+ any setting.
28
29
  - **`moduleResolution: "nodenext"`** — like `@nxgt/janus`, the package imports
29
30
  without extensions. Use `"moduleResolution": "bundler"`.
30
31
 
@@ -32,6 +33,14 @@ Nothing yet.
32
33
 
33
34
  Newest first; the [CHANGELOG](../CHANGELOG.md) holds every release.
34
35
 
36
+ - **Sign-in codes in the audit trail, v0.3.0** — a `janus.signInCode.sent`
37
+ event for every code issued, never for an address nobody holds; a sign-in
38
+ by code is a `janus.signIn` with `janus.signIn.code: true`, and the
39
+ `signInCode.confirm` span records `janus.signIn.status`, like `signIn`'s. A
40
+ refused code is a `janus.signIn.refused` warning, and every refusal of
41
+ `signInCode.confirm` is marked `janus.signIn.code: true` too, so an alert
42
+ tells it from a second factor's. Neither the code nor its
43
+ challenge is ever written. Needs `@nxgt/janus` 0.6.0.
35
44
  - **The second factor in the audit trail, v0.2.0** — `janus.signIn` records
36
45
  `janus.signIn.status`; the events `janus.signIn.secondFactor`,
37
46
  `janus.secondFactor.enrolled`, `.activated` and `.disabled`; and a refused
@@ -11,6 +11,7 @@ would be without it.
11
11
  - [`TypeError: Cannot add property …, object is not extensible` (Node), `Attempting to define property on object that is not extensible.` (Bun)](#typeerror-cannot-add-property--object-is-not-extensible)
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
+ - [A `janus.signInCode.request` span with no `janus.signInCode.sent` event](#a-janussignincoderequest-span-with-no-janussignincodesent-event)
14
15
  - [Every wrong password fails its span](#every-wrong-password-fails-its-span)
15
16
 
16
17
  ### No `janus.*` span or event at all
@@ -51,12 +52,14 @@ extensible.`
51
52
  ### A wrong password does not fail the span
52
53
 
53
54
  **When:** a `janus.signIn` span is `ok` although the sign-in threw
54
- `CREDENTIALS_INVALID` — or a `janus.secondFactor.confirm` span is `ok` although the
55
- code was refused with `CODE_INVALID`.
55
+ `CREDENTIALS_INVALID` — or a `janus.secondFactor.confirm` or
56
+ `janus.signInCode.confirm` span is `ok` although the code was refused with
57
+ `CODE_INVALID`.
56
58
 
57
59
  **Why:** a refusal is an answer — Janus worked. The span carries
58
60
  `janus.refusal`, and the `janus.signIn.refused` warning says why; for a
59
- refused code, the warning also carries `janus.secondFactor.attemptsLeft`. Only
61
+ refused code, the warning also carries `janus.secondFactor.attemptsLeft` —
62
+ under that name for an e-mailed sign-in code too. Only
60
63
  a failure, such as `STORE_FAILED`, fails a span.
61
64
 
62
65
  **Fix:** nothing. Query `janus.refusal` or the warning to count refusals.
@@ -75,9 +78,28 @@ password was right, so the user is known.)
75
78
  `unknownLogin` has none to name, and the others do not say which, so a
76
79
  response cannot tell which accounts exist. The login tried is never written.
77
80
 
81
+ The same for a refused `signInCode.confirm` with `TOKEN_UNKNOWN`,
82
+ `TOKEN_SPENT` or `TOKEN_EXPIRED`: the challenge is refused before any user is
83
+ read. Its `CODE_INVALID`, `TOKEN_STALE` and `USER_INACTIVE` do carry
84
+ `user.id`.
85
+
78
86
  **Fix:** to count refusals per account, do it where the login is known — in
79
87
  your sign-in route, from the request — not from telemetry.
80
88
 
89
+ ### A `janus.signInCode.request` span with no `janus.signInCode.sent` event
90
+
91
+ **When:** a visitor asked for a sign-in code, the span is there with
92
+ `janus.user.type` and no `user.id`, and no event follows.
93
+
94
+ **Why:** `signInCode.request` answered `null` — nobody of that type holds the
95
+ address, or the user is inactive — and nothing was sent. The event is
96
+ written only for a code issued, and never with the address typed: the trail
97
+ must not become a list of addresses tried against your users.
98
+
99
+ **Fix:** nothing, if the address is nobody's. For a user you expected to
100
+ find, see
101
+ [`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
+
81
103
  ### Every wrong password fails its span
82
104
 
83
105
  **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.2.0",
3
+ "version": "0.3.0",
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.5.0",
52
+ "@nxgt/janus": "^0.6.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.5.0",
58
+ "@nxgt/janus": "^0.6.0",
59
59
  "@nxgt/telemetry": "^0.2.1",
60
60
  "typescript": "^6.0.3"
61
61
  }