@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 +23 -15
- package/dist/flows.d.ts.map +1 -1
- package/dist/index.js +23 -2
- package/dist/index.js.map +3 -3
- package/docs/README.md +1 -1
- package/docs/guide/tracing.md +70 -7
- package/docs/roadmap.md +11 -2
- package/docs/troubleshooting.md +25 -3
- package/package.json +3 -3
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,
|
|
7
|
-
factor and who enrolled, activated or
|
|
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.
|
|
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.
|
|
75
|
-
| `janus.signIn
|
|
76
|
-
| `janus.signIn.
|
|
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
|
|
100
|
-
`USER_INACTIVE`,
|
|
101
|
-
`SECOND_FACTOR_NOT_ENROLLED`.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
package/dist/flows.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../src/flows.ts"],"names":[],"mappings":"
|
|
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=
|
|
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": "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
|
|
10
|
-
"debugId": "
|
|
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
|
|
package/docs/guide/tracing.md
CHANGED
|
@@ -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
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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`
|
|
55
|
-
code was refused with
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
58
|
+
"@nxgt/janus": "^0.6.0",
|
|
59
59
|
"@nxgt/telemetry": "^0.2.1",
|
|
60
60
|
"typescript": "^6.0.3"
|
|
61
61
|
}
|