@hexok/auth 0.0.0-stage → 0.0.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.
Files changed (46) hide show
  1. package/README.md +165 -2
  2. package/dist/authenticator.d.ts +35 -0
  3. package/dist/authenticator.d.ts.map +1 -0
  4. package/dist/authenticator.js +18 -0
  5. package/dist/authenticator.js.map +1 -0
  6. package/dist/bearer.d.ts +10 -0
  7. package/dist/bearer.d.ts.map +1 -0
  8. package/dist/bearer.js +16 -0
  9. package/dist/bearer.js.map +1 -0
  10. package/dist/config.d.ts +28 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +54 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/dev-authenticator.d.ts +27 -0
  15. package/dist/dev-authenticator.d.ts.map +1 -0
  16. package/dist/dev-authenticator.js +25 -0
  17. package/dist/dev-authenticator.js.map +1 -0
  18. package/dist/errors.d.ts +28 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +35 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/index.d.ts +11 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +11 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/memory-session-store.d.ts +27 -0
  27. package/dist/memory-session-store.d.ts.map +1 -0
  28. package/dist/memory-session-store.js +52 -0
  29. package/dist/memory-session-store.js.map +1 -0
  30. package/dist/opaque-session-token.d.ts +18 -0
  31. package/dist/opaque-session-token.d.ts.map +1 -0
  32. package/dist/opaque-session-token.js +21 -0
  33. package/dist/opaque-session-token.js.map +1 -0
  34. package/dist/resolving.d.ts +23 -0
  35. package/dist/resolving.d.ts.map +1 -0
  36. package/dist/resolving.js +34 -0
  37. package/dist/resolving.js.map +1 -0
  38. package/dist/sessions.d.ts +79 -0
  39. package/dist/sessions.d.ts.map +1 -0
  40. package/dist/sessions.js +64 -0
  41. package/dist/sessions.js.map +1 -0
  42. package/dist/static-token-authenticator.d.ts +25 -0
  43. package/dist/static-token-authenticator.d.ts.map +1 -0
  44. package/dist/static-token-authenticator.js +43 -0
  45. package/dist/static-token-authenticator.js.map +1 -0
  46. package/package.json +45 -4
package/README.md CHANGED
@@ -1,3 +1,166 @@
1
- # Temporary Holding Version
1
+ # @hexok/auth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Credential verification for hexok. Install with `npm i @hexok/auth`. It answers two questions: who does this credential prove, and is it still valid. The app answers the third, who that is to the app, in one function it owns.
4
+
5
+ The package proves an **identity**. The app resolves it to an **actor**. They are two types. The package defines neither. It has no user, organization, role, tenant, scope, `sub`, JWT, cookie, or `Request`.
6
+
7
+ ## Quick start
8
+
9
+ ```ts
10
+ import { OpaqueSessionToken, Sessions, bearerToken, sessionConfig } from '@hexok/auth';
11
+ import { RedisSessionStore } from '@hexok/auth-redis';
12
+ import { z } from 'zod';
13
+
14
+ const actor = z.object({ organizationId: z.string(), userId: z.string() });
15
+
16
+ const store = new RedisSessionStore({ redis: env.REDIS_URL, identity: actor, ...sessionConfig(env) });
17
+ await store.start();
18
+ const sessions = new Sessions({ store, token: new OpaqueSessionToken() });
19
+
20
+ // login, after the app checked the credentials it owns
21
+ const token = await sessions.open({ organizationId, userId });
22
+
23
+ // every call, at the edge
24
+ const found = await sessions.authenticate(bearerToken(request.headers.get('authorization')));
25
+ if (!found.ok) return unauthorized(); // log found.code, never send it
26
+ await useCase.execute({ actor: found.value }, input);
27
+
28
+ // logout, at the edge
29
+ await sessions.close(bearerToken(request.headers.get('authorization')));
30
+ ```
31
+
32
+ ## Rules
33
+
34
+ - Only the composition root and the edge import `@hexok/auth`. Use cases receive the app's actor on their context and never import this package. Then the package can be removed: a swap to an identity provider changes the composition root, login, and logout, and no use case.
35
+ - The actor never carries a session id or a token. Logout acts on the credential at the edge.
36
+ - A refusal is for logs. Map every refusal to one wire error, so a caller cannot tell a deleted session from a bad token. The package never picks an HTTP status.
37
+ - Authorization is not here. Roles, permissions, membership, and step-up are checks on the actor, in use-case guards.
38
+ - `Result`, `ok`, and `fail` come from `hexok`, not from this package.
39
+
40
+ ## Authenticator
41
+
42
+ ```ts
43
+ abstract class Authenticator<I, E extends string = AuthRefusal> {
44
+ authenticate(credential: string | undefined): Promise<Result<I, E>>;
45
+ }
46
+ ```
47
+
48
+ The credential is an opaque string the edge extracted, such as a Bearer token or a cookie value. `undefined` is no credential. A refusal is a `fail` result. A throw is an infrastructure failure, such as a store that does not answer. Let it reach the app's error handling as a server error, not a 401.
49
+
50
+ | `AuthRefusal` | Meaning |
51
+ | --- | --- |
52
+ | `missing` | No credential. |
53
+ | `invalid` | Malformed, a bad signature, or a token this process does not accept. |
54
+ | `expired` | The token, or the session record, passed its absolute end. |
55
+ | `ended` | The token checks out and its session is gone: logged out, idle, or deleted. |
56
+
57
+ ## Resolving the actor
58
+
59
+ `resolving` is the seam. It takes an authenticator of identity `I` and the app's function from `I` to its actor `A`, and returns an authenticator of `A`.
60
+
61
+ ```ts
62
+ import { resolving } from '@hexok/auth';
63
+ import { fail, ok } from 'hexok';
64
+
65
+ const auth = resolving(provider, async (identity) => {
66
+ const user = await users.bySubject(identity.subject);
67
+ if (user === undefined) return fail('unknown');
68
+ return ok({ organizationId: user.organizationId, userId: user.id });
69
+ });
70
+ // Authenticator<Actor, AuthRefusal | 'unknown'>
71
+ ```
72
+
73
+ - A refusal from the authenticator skips the resolver.
74
+ - The resolver names its own refusals, such as `unknown` for a subject with no account or `forbidden` for a service token on a user route.
75
+ - A resolver throw passes through as a throw.
76
+ - When the identity already is the actor, as when a session stores the actor, there is no resolver.
77
+
78
+ ## Sessions
79
+
80
+ Server-side sessions, for an app that signs its own users in. The store is the authority. A token that names a session the store does not have proves nothing, so logout and expiry are a delete.
81
+
82
+ ```ts
83
+ abstract class SessionStore<I> {
84
+ open(identity: I): Promise<{ id: string; expiresAt: Date }>;
85
+ resolve(id: string): Promise<Result<I, 'ended' | 'expired'>>;
86
+ close(id: string): Promise<void>;
87
+ }
88
+ abstract class SessionToken {
89
+ issue(id: string, expiresAt: Date): Promise<string>;
90
+ read(token: string): Promise<Result<string, 'invalid' | 'expired'>>;
91
+ }
92
+ ```
93
+
94
+ `Sessions` puts a store and a token together. It is an `Authenticator` of the stored identity.
95
+
96
+ | Method | Does |
97
+ | --- | --- |
98
+ | `open(identity)` | Opens a session under a new random id and returns the token. Login calls it after the app checked the credentials. |
99
+ | `authenticate(credential)` | Reads the token, then resolves the session in the store. The identity comes from the store, never from the token. |
100
+ | `close(credential)` | Deletes the session the credential names. A missing or bad credential is already signed out. |
101
+
102
+ A store keeps two clocks. The idle window slides on every `resolve`. The absolute end never moves. A session ends at whichever comes first. A session id is never reused, and `open` always makes a new one. The store owns the absolute end and hands it to the token as `expiresAt`, so the lifetime is configured once.
103
+
104
+ Start and stop the store you construct. `Sessions` does not own it.
105
+
106
+ | Adapter | Package | Use |
107
+ | --- | --- | --- |
108
+ | `RedisSessionStore` | `@hexok/auth-redis` | Shared sessions across processes. |
109
+ | `MemorySessionStore` | this package | Tests and single-process development. A restart ends every session. |
110
+ | `JoseSessionToken` | `@hexok/auth-jose` | A signed JWT that names the session. Checked before the store and bound to an issuer and audience. |
111
+ | `OpaqueSessionToken` | this package | The token is the session id. Tests, and one service where the token never leaves it. |
112
+
113
+ `MemorySessionStore` takes `{ idleSeconds, absoluteSeconds, now? }`. A record vanishes at the earlier of its two ends, the way a Redis key with a TTL does.
114
+
115
+ ## Static and dev authenticators
116
+
117
+ `StaticTokenAuthenticator` maps fixed tokens to identities. It fits service tokens and local development. It keeps a SHA-256 of each token, not the token. A blank token in config throws. A static token cannot be revoked except by restarting without it.
118
+
119
+ ```ts
120
+ new StaticTokenAuthenticator({ tokens: [[env.REPORTS_TOKEN, { service: 'reports' }]] });
121
+ ```
122
+
123
+ `DevAuthenticator` answers every call as one configured identity and ignores the credential. With `identity: undefined` it refuses every call, so a deploy that forgets it is closed, not open. Select it by explicit config. Never make it the default.
124
+
125
+ ```ts
126
+ const auth = env.AUTH_MODE === 'dev' ? new DevAuthenticator({ identity: devActor }) : sessions;
127
+ ```
128
+
129
+ ## Helpers
130
+
131
+ `bearerToken(header)` returns the token of an `Authorization: Bearer <token>` header. It returns `undefined` for no header, another scheme, or anything after the token. The scheme is case-insensitive, as RFC 6750 says.
132
+
133
+ `sessionConfig(env, { prefix? })` reads `AUTH_SESSION_IDLE_SECONDS` (default 1800) and `AUTH_SESSION_ABSOLUTE_SECONDS` (default 28800), or the same names under `prefix`. It takes the environment the app passes. The package never reads `process.env`. A blank value is unset. Idle longer than absolute is refused.
134
+
135
+ Every config check throws `AuthError.InvalidConfig` once, with every problem in `error.data.problems` and in the message. Secrets are never in either. `assertSessionConfig(config)` runs the same checks on values a store adapter received in code. `invalidConfig(problems)` builds the error for an adapter of your own.
136
+
137
+ `AuthError.Unauthenticated` is one error for an edge that throws on every refusal.
138
+
139
+ ## Swapping the provider
140
+
141
+ To move to an identity provider, write an `Authenticator` of that provider's identity in the app. It verifies the provider's token with the provider's SDK, then `resolving` turns that identity into the same actor.
142
+
143
+ ```ts
144
+ class ProviderAuthenticator extends Adapter(Authenticator)<ProviderIdentity> {
145
+ override async authenticate(credential: string | undefined) {
146
+ if (credential === undefined) return fail('missing');
147
+ try {
148
+ return ok(await provider.verify(credential));
149
+ } catch {
150
+ return fail('invalid');
151
+ }
152
+ }
153
+ }
154
+
155
+ const auth = resolving(new ProviderAuthenticator(), toActor);
156
+ ```
157
+
158
+ Login, logout, and the session store go away, because the provider runs them. The edge may change transport, such as from a Bearer header to a cookie. Use cases keep the same actor. To remove this package entirely, the app declares its own port with the same `authenticate` shape. Nothing outside the edge changes.
159
+
160
+ ## Left to the app
161
+
162
+ - Where credentials are stored and how passwords are checked.
163
+ - The login and logout procedures, and rate limits on login.
164
+ - The transport: header, cookie, or message field. A cookie needs a CSRF defense.
165
+ - Mapping refusals to the wire, and logging them.
166
+ - Authorization.
@@ -0,0 +1,35 @@
1
+ import { type Result } from 'hexok';
2
+ /**
3
+ * Why a credential proved nothing. It is for logs. The app maps every refusal
4
+ * to one wire error, so a caller cannot tell a deleted session from a bad token.
5
+ *
6
+ * - `missing`: no credential.
7
+ * - `invalid`: malformed, a bad signature, or a token this process does not accept.
8
+ * - `expired`: the token, or the session record, passed its absolute end.
9
+ * - `ended`: the token checks out and its session is gone: logged out, idle, or deleted.
10
+ */
11
+ export type AuthRefusal = 'missing' | 'invalid' | 'expired' | 'ended';
12
+ declare const Authenticator_base: (abstract new () => {
13
+ readonly "__#private@#brand": true;
14
+ }) & {
15
+ readonly token: "Authenticator";
16
+ };
17
+ /**
18
+ * Turns a credential into the identity it proves, or a refusal.
19
+ * The credential is an opaque string the edge extracted, such as a Bearer
20
+ * token or a cookie value. `undefined` is no credential.
21
+ *
22
+ * `I` is the identity the adapter proves. Pass it through `resolving` to get
23
+ * the app's own actor. A refusal is a result. A throw is an infrastructure
24
+ * failure, such as a store that does not answer.
25
+ *
26
+ * ```ts
27
+ * const found = await authenticator.authenticate(bearerToken(header))
28
+ * if (!found.ok) throw AuthError.Unauthenticated()
29
+ * ```
30
+ */
31
+ export declare abstract class Authenticator<I, E extends string = AuthRefusal> extends Authenticator_base {
32
+ abstract authenticate(credential: string | undefined): Promise<Result<I, E>>;
33
+ }
34
+ export {};
35
+ //# sourceMappingURL=authenticator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authenticator.d.ts","sourceRoot":"","sources":["../src/authenticator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;;;;;;AAEtE;;;;;;;;;;;;;GAaG;AACH,8BAAsB,aAAa,CACjC,CAAC,EACD,CAAC,SAAS,MAAM,GAAG,WAAW,CAC9B,SAAQ,kBAAqB;IAC7B,QAAQ,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC7E"}
@@ -0,0 +1,18 @@
1
+ import { Port } from 'hexok';
2
+ /**
3
+ * Turns a credential into the identity it proves, or a refusal.
4
+ * The credential is an opaque string the edge extracted, such as a Bearer
5
+ * token or a cookie value. `undefined` is no credential.
6
+ *
7
+ * `I` is the identity the adapter proves. Pass it through `resolving` to get
8
+ * the app's own actor. A refusal is a result. A throw is an infrastructure
9
+ * failure, such as a store that does not answer.
10
+ *
11
+ * ```ts
12
+ * const found = await authenticator.authenticate(bearerToken(header))
13
+ * if (!found.ok) throw AuthError.Unauthenticated()
14
+ * ```
15
+ */
16
+ export class Authenticator extends Port('Authenticator') {
17
+ }
18
+ //# sourceMappingURL=authenticator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authenticator.js","sourceRoot":"","sources":["../src/authenticator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAe,MAAM,OAAO,CAAC;AAa1C;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAgB,aAGpB,SAAQ,IAAI,CAAC,eAAe,CAAC;CAE9B"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The token of an `Authorization: Bearer <token>` header, or `undefined` for
3
+ * no header, another scheme, or anything after the token.
4
+ *
5
+ * ```ts
6
+ * await authenticator.authenticate(bearerToken(request.headers.get('authorization')))
7
+ * ```
8
+ */
9
+ export declare function bearerToken(header: string | null | undefined): string | undefined;
10
+ //# sourceMappingURL=bearer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer.d.ts","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,MAAM,GAAG,SAAS,CAGpB"}
package/dist/bearer.js ADDED
@@ -0,0 +1,16 @@
1
+ // RFC 6750 §2.1: "Bearer" 1*SP b64token. The scheme is case-insensitive.
2
+ const BEARER = /^Bearer +([A-Za-z0-9\-._~+/]+=*)$/i;
3
+ /**
4
+ * The token of an `Authorization: Bearer <token>` header, or `undefined` for
5
+ * no header, another scheme, or anything after the token.
6
+ *
7
+ * ```ts
8
+ * await authenticator.authenticate(bearerToken(request.headers.get('authorization')))
9
+ * ```
10
+ */
11
+ export function bearerToken(header) {
12
+ if (header === null || header === undefined)
13
+ return undefined;
14
+ return BEARER.exec(header)?.[1];
15
+ }
16
+ //# sourceMappingURL=bearer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer.js","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,MAAM,MAAM,GAAG,oCAAoC,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CACzB,MAAiC;IAEjC,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC9D,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1,28 @@
1
+ /** The environment the app read and passed in. The package never reads `process.env`. */
2
+ export type Env = Readonly<Record<string, string | undefined>>;
3
+ /** How long a session lives. Both clocks are seconds. */
4
+ export type SessionConfig = {
5
+ /** A session unused this long ends. Every authenticated call slides it. */
6
+ readonly idleSeconds: number;
7
+ /** A session ends this long after it opened, however busy it is. */
8
+ readonly absoluteSeconds: number;
9
+ };
10
+ /**
11
+ * Reads `<prefix>IDLE_SECONDS` (default 1800) and `<prefix>ABSOLUTE_SECONDS`
12
+ * (default 28800) from `env`. A blank value is unset. Throws
13
+ * `AuthError.InvalidConfig` naming every bad variable.
14
+ *
15
+ * ```ts
16
+ * const lifetimes = sessionConfig(process.env)
17
+ * ```
18
+ */
19
+ export declare function sessionConfig(env: Env, options?: {
20
+ readonly prefix?: string;
21
+ }): SessionConfig;
22
+ /**
23
+ * Throws `AuthError.InvalidConfig` unless both clocks are positive whole
24
+ * seconds and idle is not greater than absolute. Store adapters call it in
25
+ * their constructor.
26
+ */
27
+ export declare function assertSessionConfig(config: SessionConfig): void;
28
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,yFAAyF;AACzF,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAE/D,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC,CAAC;AAKF;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,GAAG,EACR,OAAO,GAAE;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACzC,aAAa,CAsBf;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAW/D"}
package/dist/config.js ADDED
@@ -0,0 +1,54 @@
1
+ import { invalidConfig } from './errors.js';
2
+ const DEFAULT_IDLE_SECONDS = 30 * 60;
3
+ const DEFAULT_ABSOLUTE_SECONDS = 8 * 60 * 60;
4
+ /**
5
+ * Reads `<prefix>IDLE_SECONDS` (default 1800) and `<prefix>ABSOLUTE_SECONDS`
6
+ * (default 28800) from `env`. A blank value is unset. Throws
7
+ * `AuthError.InvalidConfig` naming every bad variable.
8
+ *
9
+ * ```ts
10
+ * const lifetimes = sessionConfig(process.env)
11
+ * ```
12
+ */
13
+ export function sessionConfig(env, options = {}) {
14
+ const prefix = options.prefix ?? 'AUTH_SESSION_';
15
+ const problems = [];
16
+ const idleSeconds = positiveInteger(env, `${prefix}IDLE_SECONDS`, DEFAULT_IDLE_SECONDS, problems);
17
+ const absoluteSeconds = positiveInteger(env, `${prefix}ABSOLUTE_SECONDS`, DEFAULT_ABSOLUTE_SECONDS, problems);
18
+ if (problems.length === 0 && idleSeconds > absoluteSeconds) {
19
+ problems.push(`${prefix}IDLE_SECONDS: must not be greater than ${prefix}ABSOLUTE_SECONDS.`);
20
+ }
21
+ if (problems.length > 0)
22
+ throw invalidConfig(problems);
23
+ return { idleSeconds, absoluteSeconds };
24
+ }
25
+ /**
26
+ * Throws `AuthError.InvalidConfig` unless both clocks are positive whole
27
+ * seconds and idle is not greater than absolute. Store adapters call it in
28
+ * their constructor.
29
+ */
30
+ export function assertSessionConfig(config) {
31
+ const problems = [];
32
+ for (const key of ['idleSeconds', 'absoluteSeconds']) {
33
+ if (!Number.isSafeInteger(config[key]) || config[key] <= 0) {
34
+ problems.push(`${key}: must be a positive whole number of seconds.`);
35
+ }
36
+ }
37
+ if (problems.length === 0 && config.idleSeconds > config.absoluteSeconds) {
38
+ problems.push('idleSeconds: must not be greater than absoluteSeconds.');
39
+ }
40
+ if (problems.length > 0)
41
+ throw invalidConfig(problems);
42
+ }
43
+ function positiveInteger(env, name, fallback, problems) {
44
+ const raw = env[name]?.trim();
45
+ if (raw === undefined || raw === '')
46
+ return fallback;
47
+ const value = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
48
+ if (!Number.isSafeInteger(value) || value <= 0) {
49
+ problems.push(`${name}: must be a positive whole number of seconds.`);
50
+ return fallback;
51
+ }
52
+ return value;
53
+ }
54
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAa5C,MAAM,oBAAoB,GAAG,EAAE,GAAG,EAAE,CAAC;AACrC,MAAM,wBAAwB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAQ,EACR,UAAwC,EAAE;IAE1C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,eAAe,CAAC;IACjD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,WAAW,GAAG,eAAe,CACjC,GAAG,EACH,GAAG,MAAM,cAAc,EACvB,oBAAoB,EACpB,QAAQ,CACT,CAAC;IACF,MAAM,eAAe,GAAG,eAAe,CACrC,GAAG,EACH,GAAG,MAAM,kBAAkB,EAC3B,wBAAwB,EACxB,QAAQ,CACT,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,GAAG,eAAe,EAAE,CAAC;QAC3D,QAAQ,CAAC,IAAI,CACX,GAAG,MAAM,0CAA0C,MAAM,mBAAmB,CAC7E,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IACvD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAqB;IACvD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,GAAG,IAAI,CAAC,aAAa,EAAE,iBAAiB,CAAU,EAAE,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3D,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,+CAA+C,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,eAAe,EAAE,CAAC;QACzE,QAAQ,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,eAAe,CACtB,GAAQ,EACR,IAAY,EACZ,QAAgB,EAChB,QAAkB;IAElB,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;IAC9B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACrD,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;IAC3D,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,+CAA+C,CAAC,CAAC;QACtE,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { type Result } from 'hexok';
2
+ import { Authenticator, type AuthRefusal } from './authenticator.js';
3
+ declare const DevAuthenticator_base: (abstract new (...args: any[]) => {
4
+ start(): Promise<void>;
5
+ stop(): Promise<void>;
6
+ }) & typeof Authenticator;
7
+ /**
8
+ * Answers every call as one configured identity and ignores the credential.
9
+ * With no identity it refuses every call, so a deploy that forgets to
10
+ * configure it is closed, not open. Select it by explicit config for local
11
+ * development. Never make it the default.
12
+ *
13
+ * ```ts
14
+ * const auth = env.AUTH_MODE === 'dev'
15
+ * ? new DevAuthenticator({ identity: devActor })
16
+ * : sessions
17
+ * ```
18
+ */
19
+ export declare class DevAuthenticator<I> extends DevAuthenticator_base<I> {
20
+ #private;
21
+ constructor(config: {
22
+ readonly identity: I | undefined;
23
+ });
24
+ authenticate(_credential: string | undefined): Promise<Result<I, AuthRefusal>>;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=dev-authenticator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-authenticator.d.ts","sourceRoot":"","sources":["../src/dev-authenticator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;;;;;AAErE;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAgB,CAAC,CAAC,CAAE,SAAQ,sBAAuB,CAAC,CAAC;;gBAGpD,MAAM,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,CAAA;KAAE;IAKzC,YAAY,CACzB,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;CAGnC"}
@@ -0,0 +1,25 @@
1
+ import { Adapter, fail, ok } from 'hexok';
2
+ import { Authenticator } from './authenticator.js';
3
+ /**
4
+ * Answers every call as one configured identity and ignores the credential.
5
+ * With no identity it refuses every call, so a deploy that forgets to
6
+ * configure it is closed, not open. Select it by explicit config for local
7
+ * development. Never make it the default.
8
+ *
9
+ * ```ts
10
+ * const auth = env.AUTH_MODE === 'dev'
11
+ * ? new DevAuthenticator({ identity: devActor })
12
+ * : sessions
13
+ * ```
14
+ */
15
+ export class DevAuthenticator extends Adapter(Authenticator) {
16
+ #identity;
17
+ constructor(config) {
18
+ super();
19
+ this.#identity = config.identity;
20
+ }
21
+ async authenticate(_credential) {
22
+ return this.#identity === undefined ? fail('invalid') : ok(this.#identity);
23
+ }
24
+ }
25
+ //# sourceMappingURL=dev-authenticator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-authenticator.js","sourceRoot":"","sources":["../src/dev-authenticator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAe,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,gBAAoB,SAAQ,OAAO,CAAC,aAAa,CAAI;IACvD,SAAS,CAAgB;IAElC,YAAY,MAA4C;QACtD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC;IACnC,CAAC;IAEQ,KAAK,CAAC,YAAY,CACzB,WAA+B;QAE/B,OAAO,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7E,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ import { type StandardSchemaV1 } from 'hexok';
2
+ declare const AuthError_base: import("hexok").ErrorsHandle<"auth", {
3
+ readonly Unauthenticated: {
4
+ readonly message: "The call did not prove who it is.";
5
+ };
6
+ readonly InvalidConfig: {
7
+ readonly message: "The auth config is not valid.";
8
+ readonly data: StandardSchemaV1<{
9
+ problems: readonly string[];
10
+ }, {
11
+ problems: readonly string[];
12
+ }>;
13
+ };
14
+ }>;
15
+ /**
16
+ * `Unauthenticated` is for an edge that throws one error for every refusal.
17
+ * `InvalidConfig` lists every bad setting at once, so a process that fails to
18
+ * start says why.
19
+ */
20
+ export declare class AuthError extends AuthError_base {
21
+ }
22
+ /** The `InvalidConfig` error for `list`, with every problem in the message. */
23
+ export declare function invalidConfig(list: readonly string[]): import("hexok").CatalogMember<"auth", "InvalidConfig", `The auth config is not valid:
24
+ ${string}`, {
25
+ problems: readonly string[];
26
+ }>;
27
+ export {};
28
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAC;;;;;;;;sBAET,SAAS,MAAM,EAAE;;sBAAjB,SAAS,MAAM,EAAE;;;;AAmB9D;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAM7B;CAAG;AAEL,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE;;cAjCR,SAAS,MAAM,EAAE;GAsC7D"}
package/dist/errors.js ADDED
@@ -0,0 +1,35 @@
1
+ import { Errors } from 'hexok';
2
+ const problems = {
3
+ '~standard': {
4
+ version: 1,
5
+ vendor: 'hexok-auth',
6
+ validate(value) {
7
+ if (typeof value === 'object' &&
8
+ value !== null &&
9
+ 'problems' in value &&
10
+ Array.isArray(value.problems) &&
11
+ value.problems.every((each) => typeof each === 'string')) {
12
+ return { value: value };
13
+ }
14
+ return { issues: [{ message: 'problems must be a list of strings' }] };
15
+ },
16
+ },
17
+ };
18
+ /**
19
+ * `Unauthenticated` is for an edge that throws one error for every refusal.
20
+ * `InvalidConfig` lists every bad setting at once, so a process that fails to
21
+ * start says why.
22
+ */
23
+ export class AuthError extends Errors('auth', {
24
+ Unauthenticated: { message: 'The call did not prove who it is.' },
25
+ InvalidConfig: {
26
+ message: 'The auth config is not valid.',
27
+ data: problems,
28
+ },
29
+ }) {
30
+ }
31
+ /** The `InvalidConfig` error for `list`, with every problem in the message. */
32
+ export function invalidConfig(list) {
33
+ return AuthError.InvalidConfig(`The auth config is not valid:\n ${list.join('\n ')}`, { problems: list });
34
+ }
35
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAyB,MAAM,OAAO,CAAC;AAEtD,MAAM,QAAQ,GAAsD;IAClE,WAAW,EAAE;QACX,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,YAAY;QACpB,QAAQ,CAAC,KAAK;YACZ,IACE,OAAO,KAAK,KAAK,QAAQ;gBACzB,KAAK,KAAK,IAAI;gBACd,UAAU,IAAI,KAAK;gBACnB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;gBAC7B,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EACxD,CAAC;gBACD,OAAO,EAAE,KAAK,EAAE,KAAwC,EAAE,CAAC;YAC7D,CAAC;YACD,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC,EAAE,CAAC;QACzE,CAAC;KACF;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,OAAO,SAAU,SAAQ,MAAM,CAAC,MAAM,EAAE;IAC5C,eAAe,EAAE,EAAE,OAAO,EAAE,mCAAmC,EAAE;IACjE,aAAa,EAAE;QACb,OAAO,EAAE,+BAA+B;QACxC,IAAI,EAAE,QAAQ;KACf;CACF,CAAC;CAAG;AAEL,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,OAAO,SAAS,CAAC,aAAa,CAC5B,oCAAoC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EACvD,EAAE,QAAQ,EAAE,IAAI,EAAE,CACnB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ export { Authenticator, type AuthRefusal } from './authenticator.js';
2
+ export { bearerToken } from './bearer.js';
3
+ export { assertSessionConfig, type Env, type SessionConfig, sessionConfig, } from './config.js';
4
+ export { DevAuthenticator } from './dev-authenticator.js';
5
+ export { AuthError, invalidConfig } from './errors.js';
6
+ export { MemorySessionStore } from './memory-session-store.js';
7
+ export { OpaqueSessionToken } from './opaque-session-token.js';
8
+ export { type Resolve, resolving } from './resolving.js';
9
+ export { type OpenedSession, SessionStore, Sessions, SessionToken, } from './sessions.js';
10
+ export { StaticTokenAuthenticator } from './static-token-authenticator.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,mBAAmB,EACnB,KAAK,GAAG,EACR,KAAK,aAAa,EAClB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,KAAK,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EACL,KAAK,aAAa,EAClB,YAAY,EACZ,QAAQ,EACR,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { Authenticator } from './authenticator.js';
2
+ export { bearerToken } from './bearer.js';
3
+ export { assertSessionConfig, sessionConfig, } from './config.js';
4
+ export { DevAuthenticator } from './dev-authenticator.js';
5
+ export { AuthError, invalidConfig } from './errors.js';
6
+ export { MemorySessionStore } from './memory-session-store.js';
7
+ export { OpaqueSessionToken } from './opaque-session-token.js';
8
+ export { resolving } from './resolving.js';
9
+ export { SessionStore, Sessions, SessionToken, } from './sessions.js';
10
+ export { StaticTokenAuthenticator } from './static-token-authenticator.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,mBAAmB,EAGnB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAgB,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAEL,YAAY,EACZ,QAAQ,EACR,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { type Result } from 'hexok';
2
+ import { type SessionConfig } from './config.js';
3
+ import { type OpenedSession, SessionStore } from './sessions.js';
4
+ declare const MemorySessionStore_base: (abstract new (...args: any[]) => {
5
+ start(): Promise<void>;
6
+ stop(): Promise<void>;
7
+ }) & typeof SessionStore;
8
+ /**
9
+ * Sessions in this process's memory, with the same two clocks as a shared
10
+ * store. A restart ends every session, and another process cannot see them.
11
+ * Use it in tests and in single-process development only.
12
+ *
13
+ * A record vanishes at the earlier of its idle and absolute ends, the way a
14
+ * Redis key with a TTL does, so `resolve` reports both as `ended`.
15
+ */
16
+ export declare class MemorySessionStore<I> extends MemorySessionStore_base<I> {
17
+ #private;
18
+ constructor(config: SessionConfig & {
19
+ /** Milliseconds since the epoch. Tests pass a fake clock. */
20
+ readonly now?: () => number;
21
+ });
22
+ open(identity: I): Promise<OpenedSession>;
23
+ resolve(id: string): Promise<Result<I, 'ended' | 'expired'>>;
24
+ close(id: string): Promise<void>;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=memory-session-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-session-store.d.ts","sourceRoot":"","sources":["../src/memory-session-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,KAAK,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;;;;;AAQjE;;;;;;;GAOG;AACH,qBAAa,kBAAkB,CAAC,CAAC,CAAE,SAAQ,wBAAsB,CAAC,CAAC;;gBAO/D,MAAM,EAAE,aAAa,GAAG;QACtB,6DAA6D;QAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;KAC7B;IASY,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAYzC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC,CAAC;IAY5D,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
@@ -0,0 +1,52 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { Adapter, fail, ok } from 'hexok';
3
+ import { assertSessionConfig } from './config.js';
4
+ import { SessionStore } from './sessions.js';
5
+ /**
6
+ * Sessions in this process's memory, with the same two clocks as a shared
7
+ * store. A restart ends every session, and another process cannot see them.
8
+ * Use it in tests and in single-process development only.
9
+ *
10
+ * A record vanishes at the earlier of its idle and absolute ends, the way a
11
+ * Redis key with a TTL does, so `resolve` reports both as `ended`.
12
+ */
13
+ export class MemorySessionStore extends Adapter(SessionStore) {
14
+ #sessions = new Map();
15
+ #idleMs;
16
+ #absoluteMs;
17
+ #now;
18
+ constructor(config) {
19
+ super();
20
+ assertSessionConfig(config);
21
+ this.#idleMs = config.idleSeconds * 1000;
22
+ this.#absoluteMs = config.absoluteSeconds * 1000;
23
+ this.#now = config.now ?? Date.now;
24
+ }
25
+ async open(identity) {
26
+ const id = randomBytes(32).toString('base64url');
27
+ const now = this.#now();
28
+ const absoluteEnd = now + this.#absoluteMs;
29
+ this.#sessions.set(id, {
30
+ identity,
31
+ absoluteEnd,
32
+ idleUntil: Math.min(now + this.#idleMs, absoluteEnd),
33
+ });
34
+ return { id, expiresAt: new Date(absoluteEnd) };
35
+ }
36
+ async resolve(id) {
37
+ const entry = this.#sessions.get(id);
38
+ if (entry === undefined)
39
+ return fail('ended');
40
+ const now = this.#now();
41
+ if (now >= entry.idleUntil) {
42
+ this.#sessions.delete(id);
43
+ return fail('ended');
44
+ }
45
+ entry.idleUntil = Math.min(now + this.#idleMs, entry.absoluteEnd);
46
+ return ok(entry.identity);
47
+ }
48
+ async close(id) {
49
+ this.#sessions.delete(id);
50
+ }
51
+ }
52
+ //# sourceMappingURL=memory-session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-session-store.js","sourceRoot":"","sources":["../src/memory-session-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAe,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AACtE,OAAO,EAAsB,YAAY,EAAE,MAAM,eAAe,CAAC;AAQjE;;;;;;;GAOG;AACH,MAAM,OAAO,kBAAsB,SAAQ,OAAO,CAAC,YAAY,CAAI;IACxD,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;IACxC,OAAO,CAAS;IAChB,WAAW,CAAS;IACpB,IAAI,CAAe;IAE5B,YACE,MAGC;QAED,KAAK,EAAE,CAAC;QACR,mBAAmB,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC;QACzC,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,eAAe,GAAG,IAAI,CAAC;QACjD,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACrC,CAAC;IAEQ,KAAK,CAAC,IAAI,CAAC,QAAW;QAC7B,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,MAAM,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC;QAC3C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE;YACrB,QAAQ;YACR,WAAW;YACX,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC;SACrD,CAAC,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;IAClD,CAAC;IAEQ,KAAK,CAAC,OAAO,CAAC,EAAU;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;QACD,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;QAClE,OAAO,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAEQ,KAAK,CAAC,KAAK,CAAC,EAAU;QAC7B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ import { type Result } from 'hexok';
2
+ import { SessionToken } from './sessions.js';
3
+ declare const OpaqueSessionToken_base: (abstract new (...args: any[]) => {
4
+ start(): Promise<void>;
5
+ stop(): Promise<void>;
6
+ }) & typeof SessionToken;
7
+ /**
8
+ * The token is the session id itself. The store decides everything, so a
9
+ * token is only as good as the store's lookup. Use it in tests, and in one
10
+ * service where the token never leaves that service. A signed token adds a
11
+ * check before the store and binds the token to an issuer and audience.
12
+ */
13
+ export declare class OpaqueSessionToken extends OpaqueSessionToken_base {
14
+ issue(id: string, _expiresAt: Date): Promise<string>;
15
+ read(token: string): Promise<Result<string, 'invalid' | 'expired'>>;
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=opaque-session-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque-session-token.d.ts","sourceRoot":"","sources":["../src/opaque-session-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;;;;;AAK7C;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,uBAAqB;IAC5C,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAIpD,IAAI,CACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;CAIlD"}
@@ -0,0 +1,21 @@
1
+ import { Adapter, fail, ok } from 'hexok';
2
+ import { SessionToken } from './sessions.js';
3
+ // A session id is 43 characters. Anything far longer is not one, and is refused before it reaches a store.
4
+ const MAX_LENGTH = 512;
5
+ /**
6
+ * The token is the session id itself. The store decides everything, so a
7
+ * token is only as good as the store's lookup. Use it in tests, and in one
8
+ * service where the token never leaves that service. A signed token adds a
9
+ * check before the store and binds the token to an issuer and audience.
10
+ */
11
+ export class OpaqueSessionToken extends Adapter(SessionToken) {
12
+ async issue(id, _expiresAt) {
13
+ return id;
14
+ }
15
+ async read(token) {
16
+ if (token === '' || token.length > MAX_LENGTH)
17
+ return fail('invalid');
18
+ return ok(token);
19
+ }
20
+ }
21
+ //# sourceMappingURL=opaque-session-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque-session-token.js","sourceRoot":"","sources":["../src/opaque-session-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAe,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,2GAA2G;AAC3G,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB;;;;;GAKG;AACH,MAAM,OAAO,kBAAmB,SAAQ,OAAO,CAAC,YAAY,CAAC;IAClD,KAAK,CAAC,KAAK,CAAC,EAAU,EAAE,UAAgB;QAC/C,OAAO,EAAE,CAAC;IACZ,CAAC;IAEQ,KAAK,CAAC,IAAI,CACjB,KAAa;QAEb,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC,MAAM,GAAG,UAAU;YAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC;QACtE,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ import { type Result } from 'hexok';
2
+ import { Authenticator } from './authenticator.js';
3
+ /**
4
+ * Turns a proven identity into the app's actor, or a refusal the app names,
5
+ * such as `unknown` for a subject with no account or `forbidden` for a
6
+ * service token on a user route. A throw is an infrastructure failure.
7
+ */
8
+ export type Resolve<I, A, R extends string> = (identity: I) => Promise<Result<A, R>>;
9
+ /**
10
+ * The seam between proving a credential and knowing the caller. The result is
11
+ * an `Authenticator` of the app's actor `A`. Its refusals are the
12
+ * authenticator's and the resolver's. A refusal from the authenticator skips
13
+ * the resolver.
14
+ *
15
+ * ```ts
16
+ * const auth = resolving(sessions, async (identity) => {
17
+ * const user = await users.bySubject(identity.subject)
18
+ * return user === undefined ? fail('unknown') : ok({ userId: user.id })
19
+ * })
20
+ * ```
21
+ */
22
+ export declare function resolving<I, A, E extends string, R extends string>(authenticator: Authenticator<I, E>, resolve: Resolve<I, A, R>): Authenticator<A, E | R>;
23
+ //# sourceMappingURL=resolving.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolving.d.ts","sourceRoot":"","sources":["../src/resolving.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,CAC5C,QAAQ,EAAE,CAAC,KACR,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AA0B3B;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EAChE,aAAa,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAClC,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAEzB"}
@@ -0,0 +1,34 @@
1
+ import { Adapter } from 'hexok';
2
+ import { Authenticator } from './authenticator.js';
3
+ class ResolvingAuthenticator extends Adapter(Authenticator) {
4
+ #authenticator;
5
+ #resolve;
6
+ constructor(authenticator, resolve) {
7
+ super();
8
+ this.#authenticator = authenticator;
9
+ this.#resolve = resolve;
10
+ }
11
+ async authenticate(credential) {
12
+ const found = await this.#authenticator.authenticate(credential);
13
+ if (!found.ok)
14
+ return found;
15
+ return this.#resolve(found.value);
16
+ }
17
+ }
18
+ /**
19
+ * The seam between proving a credential and knowing the caller. The result is
20
+ * an `Authenticator` of the app's actor `A`. Its refusals are the
21
+ * authenticator's and the resolver's. A refusal from the authenticator skips
22
+ * the resolver.
23
+ *
24
+ * ```ts
25
+ * const auth = resolving(sessions, async (identity) => {
26
+ * const user = await users.bySubject(identity.subject)
27
+ * return user === undefined ? fail('unknown') : ok({ userId: user.id })
28
+ * })
29
+ * ```
30
+ */
31
+ export function resolving(authenticator, resolve) {
32
+ return new ResolvingAuthenticator(authenticator, resolve);
33
+ }
34
+ //# sourceMappingURL=resolving.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolving.js","sourceRoot":"","sources":["../src/resolving.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAe,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAWnD,MAAM,sBAKJ,SAAQ,OAAO,CAAC,aAAa,CAAW;IAC/B,cAAc,CAAsB;IACpC,QAAQ,CAAmB;IAEpC,YAAY,aAAkC,EAAE,OAAyB;QACvE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;QACpC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAEQ,KAAK,CAAC,YAAY,CACzB,UAA8B;QAE9B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QACjE,IAAI,CAAC,KAAK,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;QAC5B,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CACvB,aAAkC,EAClC,OAAyB;IAEzB,OAAO,IAAI,sBAAsB,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { type Result } from 'hexok';
2
+ import { Authenticator, type AuthRefusal } from './authenticator.js';
3
+ /** A session a store just opened. `expiresAt` is its absolute end. */
4
+ export type OpenedSession = {
5
+ readonly id: string;
6
+ readonly expiresAt: Date;
7
+ };
8
+ declare const SessionStore_base: (abstract new () => {
9
+ readonly "__#private@#brand": true;
10
+ }) & {
11
+ readonly token: "SessionStore";
12
+ };
13
+ /**
14
+ * Server-side sessions. The store is the authority: a token that names a
15
+ * session the store does not have proves nothing.
16
+ *
17
+ * A store keeps two clocks. The idle window slides on every `resolve`. The
18
+ * absolute end never moves. A session ends at whichever comes first.
19
+ */
20
+ export declare abstract class SessionStore<I> extends SessionStore_base {
21
+ /** Opens a session for `identity` under a new random id. It never reuses an id. */
22
+ abstract open(identity: I): Promise<OpenedSession>;
23
+ /**
24
+ * The identity of a live session, and slides its idle window, never past
25
+ * the absolute end. `ended` when no live session has the id. `expired` when
26
+ * the record outlived its absolute end, which the store then deletes.
27
+ */
28
+ abstract resolve(id: string): Promise<Result<I, 'ended' | 'expired'>>;
29
+ /** Deletes the session. A missing one is already closed. */
30
+ abstract close(id: string): Promise<void>;
31
+ }
32
+ declare const SessionToken_base: (abstract new () => {
33
+ readonly "__#private@#brand": true;
34
+ }) & {
35
+ readonly token: "SessionToken";
36
+ };
37
+ /**
38
+ * Translates a session id to and from the string the client holds.
39
+ * It knows nothing about the identity or the store.
40
+ */
41
+ export declare abstract class SessionToken extends SessionToken_base {
42
+ /** The token for `id`. `expiresAt` is the session's absolute end. */
43
+ abstract issue(id: string, expiresAt: Date): Promise<string>;
44
+ /** The session id `token` names, or why the token is not accepted. */
45
+ abstract read(token: string): Promise<Result<string, 'invalid' | 'expired'>>;
46
+ }
47
+ declare const Sessions_base: (abstract new (...args: any[]) => {
48
+ start(): Promise<void>;
49
+ stop(): Promise<void>;
50
+ }) & typeof Authenticator;
51
+ /**
52
+ * Login, logout, and authentication over a store and a token.
53
+ * It is an `Authenticator` of the stored identity.
54
+ *
55
+ * The identity comes from the store, never from the token. Logout acts on the
56
+ * credential, so the identity never carries a session id. Start and stop the
57
+ * store you construct. `Sessions` does not own it.
58
+ *
59
+ * ```ts
60
+ * const sessions = new Sessions({ store, token })
61
+ * const token = await sessions.open({ userId }) // login
62
+ * const found = await sessions.authenticate(credential) // each call
63
+ * await sessions.close(credential) // logout
64
+ * ```
65
+ */
66
+ export declare class Sessions<I> extends Sessions_base<I> {
67
+ #private;
68
+ constructor(deps: {
69
+ readonly store: SessionStore<I>;
70
+ readonly token: SessionToken;
71
+ });
72
+ /** Opens a session for `identity` and returns the token the client holds. */
73
+ open(identity: I): Promise<string>;
74
+ /** Ends the session the credential names. A missing or bad credential is already signed out. */
75
+ close(credential: string | undefined): Promise<void>;
76
+ authenticate(credential: string | undefined): Promise<Result<I, AuthRefusal>>;
77
+ }
78
+ export {};
79
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAErE,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;CAC1B,CAAC;;;;;;AAEF;;;;;;GAMG;AACH,8BAAsB,YAAY,CAAC,CAAC,CAAE,SAAQ,iBAAoB;IAChE,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAElD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC,CAAC;IAErE,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAC1C;;;;;;AAED;;;GAGG;AACH,8BAAsB,YAAa,SAAQ,iBAAoB;IAC7D,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAE5D,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;CAC7E;;;;;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,QAAQ,CAAC,CAAC,CAAE,SAAQ,cAAuB,CAAC,CAAC;;gBAI5C,IAAI,EAAE;QAChB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;QAChC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;KAC9B;IAMD,6EAA6E;IACvE,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAKxC,gGAAgG;IAC1F,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3C,YAAY,CACzB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;CAMnC"}
@@ -0,0 +1,64 @@
1
+ import { Adapter, fail, Port } from 'hexok';
2
+ import { Authenticator } from './authenticator.js';
3
+ /**
4
+ * Server-side sessions. The store is the authority: a token that names a
5
+ * session the store does not have proves nothing.
6
+ *
7
+ * A store keeps two clocks. The idle window slides on every `resolve`. The
8
+ * absolute end never moves. A session ends at whichever comes first.
9
+ */
10
+ export class SessionStore extends Port('SessionStore') {
11
+ }
12
+ /**
13
+ * Translates a session id to and from the string the client holds.
14
+ * It knows nothing about the identity or the store.
15
+ */
16
+ export class SessionToken extends Port('SessionToken') {
17
+ }
18
+ /**
19
+ * Login, logout, and authentication over a store and a token.
20
+ * It is an `Authenticator` of the stored identity.
21
+ *
22
+ * The identity comes from the store, never from the token. Logout acts on the
23
+ * credential, so the identity never carries a session id. Start and stop the
24
+ * store you construct. `Sessions` does not own it.
25
+ *
26
+ * ```ts
27
+ * const sessions = new Sessions({ store, token })
28
+ * const token = await sessions.open({ userId }) // login
29
+ * const found = await sessions.authenticate(credential) // each call
30
+ * await sessions.close(credential) // logout
31
+ * ```
32
+ */
33
+ export class Sessions extends Adapter(Authenticator) {
34
+ #store;
35
+ #token;
36
+ constructor(deps) {
37
+ super();
38
+ this.#store = deps.store;
39
+ this.#token = deps.token;
40
+ }
41
+ /** Opens a session for `identity` and returns the token the client holds. */
42
+ async open(identity) {
43
+ const session = await this.#store.open(identity);
44
+ return this.#token.issue(session.id, session.expiresAt);
45
+ }
46
+ /** Ends the session the credential names. A missing or bad credential is already signed out. */
47
+ async close(credential) {
48
+ if (credential === undefined || credential === '')
49
+ return;
50
+ const id = await this.#token.read(credential);
51
+ if (!id.ok)
52
+ return;
53
+ await this.#store.close(id.value);
54
+ }
55
+ async authenticate(credential) {
56
+ if (credential === undefined || credential === '')
57
+ return fail('missing');
58
+ const id = await this.#token.read(credential);
59
+ if (!id.ok)
60
+ return fail(id.code);
61
+ return this.#store.resolve(id.value);
62
+ }
63
+ }
64
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAe,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAC;AAQrE;;;;;;GAMG;AACH,MAAM,OAAgB,YAAgB,SAAQ,IAAI,CAAC,cAAc,CAAC;CAajE;AAED;;;GAGG;AACH,MAAM,OAAgB,YAAa,SAAQ,IAAI,CAAC,cAAc,CAAC;CAM9D;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,QAAY,SAAQ,OAAO,CAAC,aAAa,CAAI;IAC/C,MAAM,CAAkB;IACxB,MAAM,CAAe;IAE9B,YAAY,IAGX;QACC,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;IAC3B,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,IAAI,CAAC,QAAW;QACpB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED,gGAAgG;IAChG,KAAK,CAAC,KAAK,CAAC,UAA8B;QACxC,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,EAAE;YAAE,OAAO;QAC1D,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE,CAAC,EAAE;YAAE,OAAO;QACnB,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAEQ,KAAK,CAAC,YAAY,CACzB,UAA8B;QAE9B,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ import { type Result } from 'hexok';
2
+ import { Authenticator, type AuthRefusal } from './authenticator.js';
3
+ declare const StaticTokenAuthenticator_base: (abstract new (...args: any[]) => {
4
+ start(): Promise<void>;
5
+ stop(): Promise<void>;
6
+ }) & typeof Authenticator;
7
+ /**
8
+ * Fixed tokens, each mapped to an identity. It fits service tokens and local
9
+ * development. The map is keyed by a SHA-256 of each token, so the raw tokens
10
+ * are not kept, and a lookup does not compare token strings. A token cannot
11
+ * be revoked except by restarting without it.
12
+ *
13
+ * ```ts
14
+ * new StaticTokenAuthenticator({ tokens: [[process.env.REPORTS_TOKEN, { service: 'reports' }]] })
15
+ * ```
16
+ */
17
+ export declare class StaticTokenAuthenticator<I> extends StaticTokenAuthenticator_base<I> {
18
+ #private;
19
+ constructor(config: {
20
+ readonly tokens: Iterable<readonly [token: string, identity: I]>;
21
+ });
22
+ authenticate(credential: string | undefined): Promise<Result<I, AuthRefusal>>;
23
+ }
24
+ export {};
25
+ //# sourceMappingURL=static-token-authenticator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-token-authenticator.d.ts","sourceRoot":"","sources":["../src/static-token-authenticator.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;;;;;AAGrE;;;;;;;;;GASG;AACH,qBAAa,wBAAwB,CAAC,CAAC,CAAE,SAAQ,8BAAuB,CAAC,CAAC;;gBAG5D,MAAM,EAAE;QAClB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;KAClE;IAYc,YAAY,CACzB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;CAMnC"}
@@ -0,0 +1,43 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { Adapter, fail, ok } from 'hexok';
3
+ import { Authenticator } from './authenticator.js';
4
+ import { invalidConfig } from './errors.js';
5
+ /**
6
+ * Fixed tokens, each mapped to an identity. It fits service tokens and local
7
+ * development. The map is keyed by a SHA-256 of each token, so the raw tokens
8
+ * are not kept, and a lookup does not compare token strings. A token cannot
9
+ * be revoked except by restarting without it.
10
+ *
11
+ * ```ts
12
+ * new StaticTokenAuthenticator({ tokens: [[process.env.REPORTS_TOKEN, { service: 'reports' }]] })
13
+ * ```
14
+ */
15
+ export class StaticTokenAuthenticator extends Adapter(Authenticator) {
16
+ #identities = new Map();
17
+ constructor(config) {
18
+ super();
19
+ const problems = [];
20
+ let index = 0;
21
+ for (const [token, identity] of config.tokens) {
22
+ if (token.trim() === '')
23
+ problems.push(`tokens[${index}]: is blank.`);
24
+ else
25
+ this.#identities.set(digest(token), identity);
26
+ index += 1;
27
+ }
28
+ if (problems.length > 0)
29
+ throw invalidConfig(problems);
30
+ }
31
+ async authenticate(credential) {
32
+ if (credential === undefined || credential === '')
33
+ return fail('missing');
34
+ const key = digest(credential);
35
+ if (!this.#identities.has(key))
36
+ return fail('invalid');
37
+ return ok(this.#identities.get(key));
38
+ }
39
+ }
40
+ function digest(token) {
41
+ return createHash('sha256').update(token).digest('base64url');
42
+ }
43
+ //# sourceMappingURL=static-token-authenticator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-token-authenticator.js","sourceRoot":"","sources":["../src/static-token-authenticator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAe,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;GASG;AACH,MAAM,OAAO,wBAA4B,SAAQ,OAAO,CAAC,aAAa,CAAI;IAC/D,WAAW,GAAG,IAAI,GAAG,EAAa,CAAC;IAE5C,YAAY,MAEX;QACC,KAAK,EAAE,CAAC;QACR,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAC9C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,KAAK,cAAc,CAAC,CAAC;;gBACjE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;YACnD,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzD,CAAC;IAEQ,KAAK,CAAC,YAAY,CACzB,UAA8B;QAE9B,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC;QACvD,OAAO,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAM,CAAC,CAAC;IAC5C,CAAC;CACF;AAED,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAChE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@hexok/auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.0",
4
+ "description": "Credential verification for hexok: an Authenticator port, sessions, and a seam that resolves the app's own actor.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "keywords": [
9
+ "hexok",
10
+ "auth",
11
+ "authentication",
12
+ "session"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/crobinson42/hexok.git",
17
+ "directory": "packages/auth"
18
+ },
19
+ "homepage": "https://crobinson42.github.io/hexok/",
20
+ "bugs": {
21
+ "url": "https://github.com/crobinson42/hexok/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc -b",
39
+ "test": "vitest run",
40
+ "type-check": "tsc --noEmit -p tsconfig.check.json",
41
+ "lint": "biome check .",
42
+ "clean": "rm -rf dist *.tsbuildinfo"
43
+ },
44
+ "dependencies": {
45
+ "hexok": "^2.2.5"
46
+ }
47
+ }