@owlmeans/server-auth-token 0.1.18-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +15 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/server-auth-token/SKILL.md +136 -0
  4. package/build/append.d.ts +20 -0
  5. package/build/append.d.ts.map +1 -0
  6. package/build/append.js +29 -0
  7. package/build/append.js.map +1 -0
  8. package/build/coguard.d.ts +16 -0
  9. package/build/coguard.d.ts.map +1 -0
  10. package/build/coguard.js +23 -0
  11. package/build/coguard.js.map +1 -0
  12. package/build/consts.d.ts +3 -0
  13. package/build/consts.d.ts.map +1 -0
  14. package/build/consts.js +3 -0
  15. package/build/consts.js.map +1 -0
  16. package/build/guard.d.ts +24 -0
  17. package/build/guard.d.ts.map +1 -0
  18. package/build/guard.js +126 -0
  19. package/build/guard.js.map +1 -0
  20. package/build/handlers/index.d.ts +4 -0
  21. package/build/handlers/index.d.ts.map +1 -0
  22. package/build/handlers/index.js +94 -0
  23. package/build/handlers/index.js.map +1 -0
  24. package/build/hash.d.ts +17 -0
  25. package/build/hash.d.ts.map +1 -0
  26. package/build/hash.js +19 -0
  27. package/build/hash.js.map +1 -0
  28. package/build/index.d.ts +9 -0
  29. package/build/index.d.ts.map +1 -0
  30. package/build/index.js +8 -0
  31. package/build/index.js.map +1 -0
  32. package/build/resource.d.ts +14 -0
  33. package/build/resource.d.ts.map +1 -0
  34. package/build/resource.js +23 -0
  35. package/build/resource.js.map +1 -0
  36. package/build/types.d.ts +36 -0
  37. package/build/types.d.ts.map +1 -0
  38. package/build/types.js +2 -0
  39. package/build/types.js.map +1 -0
  40. package/package.json +48 -0
  41. package/src/append.ts +34 -0
  42. package/src/coguard.ts +23 -0
  43. package/src/consts.ts +2 -0
  44. package/src/guard.ts +143 -0
  45. package/src/handlers/index.ts +115 -0
  46. package/src/hash.ts +27 -0
  47. package/src/index.ts +8 -0
  48. package/src/resource.ts +28 -0
  49. package/src/types.ts +39 -0
  50. package/tests/coguard.spec.ts +36 -0
  51. package/tests/context.ts +109 -0
  52. package/tests/guard.spec.ts +128 -0
  53. package/tests/handlers.spec.ts +154 -0
  54. package/tests/tsconfig.json +12 -0
  55. package/tsconfig.json +11 -0
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ <!-- owlmeans:agent-guidance:start -->
2
+ ## Agent guidance
3
+
4
+ This package ships embedded agent skills under `agent-meta/`. After installing your
5
+ `@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
6
+ your project's skill store (`.agents/skills/`):
7
+
8
+ ```sh
9
+ npx @owlmeans/agent-skills@^0.1.18-rc.13
10
+ ```
11
+
12
+ The embedded files are version-matched to this package release. Do not edit them
13
+ directly — they are regenerated on each publish. To contribute guidance edits,
14
+ open a PR against the source monorepo.
15
+ <!-- owlmeans:agent-guidance:end -->
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "package": "@owlmeans/server-auth-token",
4
+ "version": "0.1.18-rc.1",
5
+ "generatedAt": "2026-09-10T18:54:42.508Z",
6
+ "canonicalRepo": "https://github.com/owlmeans/common",
7
+ "entries": [
8
+ {
9
+ "kind": "skill",
10
+ "name": "server-auth-token",
11
+ "category": "package-specific",
12
+ "file": "skills/server-auth-token/SKILL.md",
13
+ "canonicalPath": ".agents/skills/server-auth-token/SKILL.md"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: server-auth-token
3
+ description: How to use @owlmeans/server-auth-token — the server half of long-lived access tokens — the Mongo store, the guard that verifies a presented token and intersects its scopes with the profile's, the mint/list/revoke handlers, and the coguard that admits a token on every already-guarded route. Auto-invoked when registering the token guard or resources, mounting the token handlers, or diagnosing a 401 on a route an access token should reach.
4
+ user-invocable: false
5
+ ---
6
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
+
8
+ # @owlmeans/server-auth-token
9
+
10
+ **Layer:** Server
11
+ **Install:** `"@owlmeans/server-auth-token": "^0.1.18-rc.1"` in `dependencies`
12
+ **Contracts:** `@owlmeans/auth-token` — the record, the routes, the format helpers
13
+
14
+ ## Key Exports
15
+
16
+ | Export | Description |
17
+ |--------|-------------|
18
+ | `appendAuthTokenResources(ctx, dbAlias?)` | Register the token store. Pass the identity resources' `dbAlias` |
19
+ | `appendAuthTokenGuard(ctx, alias?, opts?)` | Register the guard AND write `opts.prefix` into the config |
20
+ | `makeAuthTokenGuard(alias?, opts?)` | The guard itself |
21
+ | `makeAccessTokenResource(dbAlias?)` | The Mongo resource, with its three indexes |
22
+ | `prefixOf(context, opts?)` | The deployment's prefix: guard options → config → default |
23
+ | `hashAccessToken(token)` · `mintAccessToken(prefix)` | The stored form; one minted `{ token, hash, display }` |
24
+ | `setupAuthTokenCoguard(entrypoints, guard?)` | Append the guard to every entrypoint that already has one |
25
+ | `listAccessTokens` · `createAccessToken` · `revokeAccessToken` | The three handlers |
26
+ | `AuthTokenGuardOptions` (`prefix`, `denyAliases`, `touchInterval`, `resourceAlias`, `profileAlias`) · `AuthTokenConfig` · `AccessTokenResource` | Types |
27
+
28
+ ## Wiring
29
+
30
+ ```typescript
31
+ appendAuthTokenResources(context, AUTH_IDENTITY_DB_ALIAS)
32
+ appendAuthTokenGuard(context, GUARD_AUTH_TOKEN, {
33
+ prefix: 'vib_',
34
+ denyAliases: TOKEN_DENIED_ROUTES,
35
+ })
36
+ ```
37
+
38
+ **The prefix is written into the configuration, because the guard is not the only reader.** The
39
+ minting handler has no guard instance to ask and resolves it through `prefixOf(ctx)`, and the two
40
+ MUST agree: a deployment that told only the guard issues `owl_…` tokens its own guard refuses to
41
+ claim, and every request 401s with nothing anywhere saying why.
42
+
43
+ Register the store against the **same `dbAlias` as the identity resources**. The guard reads a
44
+ token's profile back on every request, and splitting the two across databases makes every
45
+ authenticated call a cross-database join nobody wrote.
46
+
47
+ ## The store
48
+
49
+ `hash` is **uniquely** indexed — two records answering one presented secret would make revocation a
50
+ coin toss. `entityId`/`profileId` are plain indexed strings, never declared ObjectId references,
51
+ because that is how the identity records they point at store them, and a reference declared on one
52
+ side of a join and not the other finds nothing. No `$jsonSchema` is applied: the write path is one
53
+ handler with a validated body, and a collection schema would have to be kept in step with a record
54
+ the guard writes to (`lastUsedAt`) on every request.
55
+
56
+ SHA-256 rather than a password hash is deliberate. The input is 192 bits of machine-generated
57
+ randomness, so there is no dictionary to slow down, and the hash is computed on **every** API
58
+ request — a deliberately slow function here would be a deliberate rate limit on the deployment's
59
+ whole authenticated surface.
60
+
61
+ ## The guard verifies; it never authenticates
62
+
63
+ `authenticated()` returns `null`. Nothing is exchanged, no envelope is signed, no session begins: a
64
+ caller presents a credential it was handed once, and the guard turns it into the same `Auth` payload
65
+ every other guard produces — so ownership gates, entitlement gates and handlers written against a
66
+ browser session work unchanged for an API client. The `token` field carries the **display** form, so
67
+ a log or an audit trail can hold it safely.
68
+
69
+ Three properties keep it from colliding with the guards beside it:
70
+
71
+ - it claims a header only when the value carries the deployment's prefix, under `AUTH-TOKEN` **or**
72
+ `Bearer`;
73
+ - **it intersects the token's scopes with its profile's on every request** — and loads the profile
74
+ per request, which is what makes revoking a person's access revoke every token they ever minted
75
+ without a single token record being touched. `ALL_SCOPES` on either side widens to the other. A
76
+ revoked token, an expired token, a missing profile and an expired profile all refuse;
77
+ - `entitySlug` is resolved through `ENTITY_RESOLVER` when one is registered, so the payload is
78
+ identical to a browser session's rather than carrying a raw id.
79
+
80
+ `lastUsedAt` is a **throttled, fire-and-forget** write (`touchInterval`, default 5 minutes). A usage
81
+ timestamp is never a reason to fail a request, and awaiting it would put a database write on the
82
+ critical path of every authenticated call. It answers one question — "is this token still in use?" —
83
+ and is not an access log.
84
+
85
+ ## A token may never mint or revoke a token
86
+
87
+ Minting is the one operation that turns a stolen credential into a permanent one: a token that can
88
+ create tokens survives the revocation of the token that leaked. `createAccessToken` and
89
+ `revokeAccessToken` refuse a request whose `auth.type` is `AuthroizationType.AuthToken`, and a
90
+ deployment additionally names those routes in the guard's **deny list**, so the refusal is a 401 at
91
+ the boundary rather than a check every future handler has to remember.
92
+
93
+ Minting only ever narrows: asking for a scope the caller does not hold is **refused**, not silently
94
+ dropped — a token that quietly grants less than it was asked for fails later, somewhere else, with
95
+ an error about the wrong thing. Revoke is idempotent (a retry looks like a second revoke), and
96
+ another profile's token answers exactly as an unknown id does, so an owner learns nothing about
97
+ tokens that are not theirs.
98
+
99
+ ## The coguard admits a token everywhere, and the deny list lives in the GUARD
100
+
101
+ ```typescript
102
+ setupAuthTokenCoguard(managerEntrypoints as Array<{ guards?: string[] }>)
103
+ ```
104
+
105
+ An API client drives the same surface a browser does — projects, stories, files — so admitting the
106
+ token only on a dedicated prefix would mean a second copy of every route. The coguard **appends**,
107
+ never substitutes: the primary guard stays first and still claims its own credential, and this one
108
+ matches only a value carrying the prefix. It skips entrypoints that have no guard, and it is
109
+ idempotent.
110
+
111
+ **Run it after the last elevation**, because it reads the guard list each entrypoint actually ended
112
+ up with.
113
+
114
+ Routes that must stay behind an interactive session — opening a checkout, starting an OAuth flow,
115
+ minting another token — are named in `AuthTokenGuardOptions.denyAliases` rather than skipped here.
116
+ A child entrypoint inherits every guard its ancestors declare and **cannot drop one**, so the only
117
+ place left to say "not with this credential" is the moment the guard decides whether the request is
118
+ its business at all. Refusing in `match` is what makes it a 401 instead of a route that quietly
119
+ works.
120
+
121
+ ## Tests
122
+
123
+ `bun test ./tests` in the package — what the guard claims, what it resolves (including that a token
124
+ can never outrank its profile), the mint/list/revoke rules, and the coguard's four properties.
125
+
126
+ ## Depends On
127
+
128
+ - `@owlmeans/auth-token` — the contracts · `@owlmeans/auth`, `@owlmeans/auth-common`
129
+ - `@owlmeans/server-auth-identity` — the profile a token is bound to
130
+ - `@owlmeans/mongo-resource`, `@owlmeans/server-api`, `@owlmeans/server-context`
131
+ - `@noble/hashes`, `@scure/base`
132
+
133
+ ## Related
134
+
135
+ - [[auth-token]] — the format, the routes and the client carrier guard
136
+ - [[web-auth-token]] — the management panel · [[auth-protocol]] · [[server-auth]]
@@ -0,0 +1,20 @@
1
+ import type { BasicContext } from '@owlmeans/context';
2
+ import type { AuthTokenGuardOptions } from './types.js';
3
+ /**
4
+ * Register the token store.
5
+ *
6
+ * Pass the same `dbAlias` the identity resources use: a token's `entityId`/`profileId` are read
7
+ * back against those collections on every request, and splitting the two across databases makes
8
+ * every lookup a cross-database join nobody wrote.
9
+ */
10
+ export declare const appendAuthTokenResources: (context: BasicContext<any>, dbAlias?: string) => void;
11
+ /**
12
+ * Register the guard that verifies a presented access token.
13
+ *
14
+ * A `prefix` passed here is written into the configuration, because the guard is not the only
15
+ * reader: the minting handler has no guard instance to ask, and the two MUST agree — a deployment
16
+ * that told only the guard would issue `owl_…` tokens its own guard refuses to claim, and every
17
+ * request would 401 with nothing anywhere saying why.
18
+ */
19
+ export declare const appendAuthTokenGuard: (context: BasicContext<any>, alias?: string, opts?: AuthTokenGuardOptions) => void;
20
+ //# sourceMappingURL=append.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAExE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,YAAa,YAAY,CAAC,GAAG,CAAC,YAAY,MAAM,KAAG,IAEvF,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,YACtB,YAAY,CAAC,GAAG,CAAC,UAAS,MAAM,SAA4B,qBAAqB,KACzF,IAMF,CAAA"}
@@ -0,0 +1,29 @@
1
+ import { GUARD_AUTH_TOKEN } from '@owlmeans/auth-token';
2
+ import { makeAuthTokenGuard } from './guard.js';
3
+ import { makeAccessTokenResource } from './resource.js';
4
+ /**
5
+ * Register the token store.
6
+ *
7
+ * Pass the same `dbAlias` the identity resources use: a token's `entityId`/`profileId` are read
8
+ * back against those collections on every request, and splitting the two across databases makes
9
+ * every lookup a cross-database join nobody wrote.
10
+ */
11
+ export const appendAuthTokenResources = (context, dbAlias) => {
12
+ context.registerResource(makeAccessTokenResource(dbAlias));
13
+ };
14
+ /**
15
+ * Register the guard that verifies a presented access token.
16
+ *
17
+ * A `prefix` passed here is written into the configuration, because the guard is not the only
18
+ * reader: the minting handler has no guard instance to ask, and the two MUST agree — a deployment
19
+ * that told only the guard would issue `owl_…` tokens its own guard refuses to claim, and every
20
+ * request would 401 with nothing anywhere saying why.
21
+ */
22
+ export const appendAuthTokenGuard = (context, alias = GUARD_AUTH_TOKEN, opts) => {
23
+ if (opts?.prefix != null) {
24
+ const cfg = context.cfg;
25
+ cfg.authToken = { ...cfg.authToken, prefix: opts.prefix };
26
+ }
27
+ context.registerService(makeAuthTokenGuard(alias, opts));
28
+ };
29
+ //# sourceMappingURL=append.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append.js","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAEvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAGvD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,OAA0B,EAAE,OAAgB,EAAQ,EAAE;IAC7F,OAAO,CAAC,gBAAgB,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAA;AAC5D,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,OAA0B,EAAE,KAAK,GAAW,gBAAgB,EAAE,IAA4B,EACpF,EAAE;IACR,IAAI,IAAI,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAsB,CAAA;QAC1C,GAAG,CAAC,SAAS,GAAG,EAAE,GAAG,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAA;IAC3D,CAAC;IACD,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;AAC1D,CAAC,CAAA"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Let an access token authenticate every route that already has a guard.
3
+ *
4
+ * An API client drives the same surface a browser does — projects, stories, files — so admitting
5
+ * the token only on a dedicated prefix would mean a second copy of every route. The coguard is
6
+ * appended, never substituted: the primary guard stays first and still claims its own credential,
7
+ * and this one matches only a value carrying the deployment's token prefix.
8
+ *
9
+ * Routes that must stay behind an interactive session are named in the GUARD's deny list rather
10
+ * than skipped here — a child entrypoint inherits every guard its ancestors declare and cannot
11
+ * drop one, so the refusal has to live where the credential is inspected.
12
+ */
13
+ export declare const setupAuthTokenCoguard: (entrypoints: Array<{
14
+ guards?: string[];
15
+ }>, guard?: string) => void;
16
+ //# sourceMappingURL=coguard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coguard.d.ts","sourceRoot":"","sources":["../src/coguard.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,gBACnB,KAAK,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,UAAS,MAAM,KACvD,IAMF,CAAA"}
@@ -0,0 +1,23 @@
1
+ import { GUARD_AUTH_TOKEN } from '@owlmeans/auth-token';
2
+ /**
3
+ * Let an access token authenticate every route that already has a guard.
4
+ *
5
+ * An API client drives the same surface a browser does — projects, stories, files — so admitting
6
+ * the token only on a dedicated prefix would mean a second copy of every route. The coguard is
7
+ * appended, never substituted: the primary guard stays first and still claims its own credential,
8
+ * and this one matches only a value carrying the deployment's token prefix.
9
+ *
10
+ * Routes that must stay behind an interactive session are named in the GUARD's deny list rather
11
+ * than skipped here — a child entrypoint inherits every guard its ancestors declare and cannot
12
+ * drop one, so the refusal has to live where the credential is inspected.
13
+ */
14
+ export const setupAuthTokenCoguard = (entrypoints, guard = GUARD_AUTH_TOKEN) => {
15
+ entrypoints.forEach(entrypoint => {
16
+ if (entrypoint.guards == null || entrypoint.guards.length < 1)
17
+ return;
18
+ if (entrypoint.guards.includes(guard))
19
+ return;
20
+ entrypoint.guards = [...entrypoint.guards, guard];
21
+ });
22
+ };
23
+ //# sourceMappingURL=coguard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coguard.js","sourceRoot":"","sources":["../src/coguard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,WAAyC,EAAE,KAAK,GAAW,gBAAgB,EACrE,EAAE;IACR,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QAC/B,IAAI,UAAU,CAAC,MAAM,IAAI,IAAI,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAM;QACrE,IAAI,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAM;QAC7C,UAAU,CAAC,MAAM,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ /** Service alias of the module, for error locations. */
2
+ export declare const SERVER_AUTH_TOKEN = "server-auth-token";
3
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,sBAAsB,CAAA"}
@@ -0,0 +1,3 @@
1
+ /** Service alias of the module, for error locations. */
2
+ export const SERVER_AUTH_TOKEN = 'server-auth-token';
3
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAA"}
@@ -0,0 +1,24 @@
1
+ import type { GuardService } from '@owlmeans/entrypoint';
2
+ import type { AuthTokenContext, AuthTokenGuardOptions } from './types.js';
3
+ /** Where the deployment's prefix comes from: the guard's own options, then config, then default. */
4
+ export declare const prefixOf: (context: AuthTokenContext, opts?: AuthTokenGuardOptions) => string;
5
+ /**
6
+ * The guard that verifies a long-lived access token.
7
+ *
8
+ * It is a GUARD, not an authentication method: nothing is exchanged, no envelope is signed, no
9
+ * session begins. A caller presents a credential it was handed once, and the guard turns it into
10
+ * the same `Auth` payload every other guard produces — so ownership gates, entitlement gates and
11
+ * handlers written against a browser session work unchanged for an API client.
12
+ *
13
+ * Three properties are what keep it from colliding with the guards it sits beside:
14
+ *
15
+ * - it claims a header only when the value carries the deployment's prefix, so an OwlMeans
16
+ * `ED25519-BASIC-TOKEN` bearer, an OIDC one, and this one can all arrive under `Authorization`
17
+ * without any of them shadowing the others;
18
+ * - it accepts the OwlMeans `AUTH-TOKEN` scheme AND plain `Bearer`, because a third-party client
19
+ * configured with a URL sends the latter whatever the documentation says;
20
+ * - it intersects the token's scopes with its profile's on every request, so a token can never
21
+ * outlive or outrank the person who minted it.
22
+ */
23
+ export declare const makeAuthTokenGuard: (alias?: string, opts?: AuthTokenGuardOptions) => GuardService;
24
+ //# sourceMappingURL=guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAqC,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAI3F,OAAO,KAAK,EAAwC,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAE/G,oGAAoG;AACpG,eAAO,MAAM,QAAQ,YAAa,gBAAgB,SAAS,qBAAqB,KAAG,MACc,CAAA;AAEjG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,WACtB,MAAM,SAA2B,qBAAqB,KAC5D,YAsGF,CAAA"}
package/build/guard.js ADDED
@@ -0,0 +1,126 @@
1
+ import { ALL_SCOPES, AuthroizationType } from '@owlmeans/auth';
2
+ import { AUTH_TOKEN_DEFAULT_PREFIX, AUTH_TOKEN_SCHEME, AUTH_TOKEN_RESOURCE, AUTH_TOKEN_TOUCH_INTERVAL, BEARER_SCHEME, GUARD_AUTH_TOKEN, isAccessToken, parseAuthorizationHeader } from '@owlmeans/auth-token';
3
+ import { AUTH_HEADER } from '@owlmeans/auth';
4
+ import { ENTITY_RESOLVER } from '@owlmeans/auth-common';
5
+ import { createService } from '@owlmeans/context';
6
+ import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity';
7
+ import { hashAccessToken } from './hash.js';
8
+ /** Where the deployment's prefix comes from: the guard's own options, then config, then default. */
9
+ export const prefixOf = (context, opts) => opts?.prefix ?? context.cfg.authToken?.prefix ?? AUTH_TOKEN_DEFAULT_PREFIX;
10
+ /**
11
+ * The guard that verifies a long-lived access token.
12
+ *
13
+ * It is a GUARD, not an authentication method: nothing is exchanged, no envelope is signed, no
14
+ * session begins. A caller presents a credential it was handed once, and the guard turns it into
15
+ * the same `Auth` payload every other guard produces — so ownership gates, entitlement gates and
16
+ * handlers written against a browser session work unchanged for an API client.
17
+ *
18
+ * Three properties are what keep it from colliding with the guards it sits beside:
19
+ *
20
+ * - it claims a header only when the value carries the deployment's prefix, so an OwlMeans
21
+ * `ED25519-BASIC-TOKEN` bearer, an OIDC one, and this one can all arrive under `Authorization`
22
+ * without any of them shadowing the others;
23
+ * - it accepts the OwlMeans `AUTH-TOKEN` scheme AND plain `Bearer`, because a third-party client
24
+ * configured with a URL sends the latter whatever the documentation says;
25
+ * - it intersects the token's scopes with its profile's on every request, so a token can never
26
+ * outlive or outrank the person who minted it.
27
+ */
28
+ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
29
+ const touchInterval = opts.touchInterval ?? AUTH_TOKEN_TOUCH_INTERVAL;
30
+ const present = (req, prefix) => {
31
+ const parsed = parseAuthorizationHeader(req.headers?.[AUTH_HEADER]);
32
+ if (parsed == null)
33
+ return null;
34
+ if (parsed.scheme !== AUTH_TOKEN_SCHEME && parsed.scheme !== BEARER_SCHEME)
35
+ return null;
36
+ return isAccessToken(parsed.value, prefix) ? parsed.value : null;
37
+ };
38
+ const service = createService(alias, {
39
+ match: async (req) => {
40
+ const context = service.assertCtx();
41
+ if (req == null)
42
+ return false;
43
+ // A denied route refuses the credential outright. The refusal has to happen here rather
44
+ // than in the handler: a guard appended to a base reaches every child, and a child cannot
45
+ // drop what it inherited — so the only place left to say "not with this credential" is the
46
+ // moment the guard decides whether the request is its business at all.
47
+ if (opts.denyAliases != null && req.alias != null && opts.denyAliases.includes(req.alias)) {
48
+ return false;
49
+ }
50
+ return present(req, prefixOf(context, opts)) != null;
51
+ },
52
+ handle: async (req, res) => {
53
+ const context = service.assertCtx();
54
+ const prefix = prefixOf(context, opts);
55
+ const presented = present(req, prefix);
56
+ if (presented == null)
57
+ return false;
58
+ const tokens = context.resource(opts.resourceAlias ?? AUTH_TOKEN_RESOURCE);
59
+ const record = await tokens.load({ hash: hashAccessToken(presented) });
60
+ if (record == null)
61
+ return false;
62
+ if (record.revokedAt != null)
63
+ return false;
64
+ if (record.expiresAt != null && new Date(record.expiresAt) < new Date())
65
+ return false;
66
+ // A token is only ever as good as the profile behind it. Reading the profile per request is
67
+ // what makes revoking a person's access revoke every token they ever minted, without any
68
+ // token record being touched.
69
+ const profiles = context.resource(opts.profileAlias ?? AUTH_IDENTITY_PROFILE);
70
+ let profile = null;
71
+ try {
72
+ profile = await profiles.load({ entityId: record.entityId, profileId: record.profileId });
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ if (profile == null)
78
+ return false;
79
+ if (profile.expiresAt != null && new Date(profile.expiresAt) < new Date())
80
+ return false;
81
+ const profileScopes = profile.scopes ?? [];
82
+ const scopes = profileScopes.includes(ALL_SCOPES)
83
+ ? record.scopes
84
+ : record.scopes.includes(ALL_SCOPES)
85
+ ? profileScopes
86
+ : record.scopes.filter(scope => profileScopes.includes(scope));
87
+ // `attachEntity` canonicalizes whatever it is given, but it resolves by NAME — so writing
88
+ // the current slug here costs one cached read and keeps the payload identical to the one a
89
+ // browser session produces.
90
+ let entitySlug = record.entityId;
91
+ if (context.hasService(ENTITY_RESOLVER)) {
92
+ const resolver = context.service(ENTITY_RESOLVER);
93
+ const entity = await resolver.byId(record.entityId);
94
+ if (entity != null)
95
+ entitySlug = entity.slug;
96
+ }
97
+ res.resolve({
98
+ // Never the secret: what a downstream reader sees is the display form, which is what a
99
+ // log or an audit trail can safely carry.
100
+ token: record.display,
101
+ type: AuthroizationType.AuthToken,
102
+ role: profile.role,
103
+ userId: record.userId,
104
+ profileId: record.profileId,
105
+ entitySlug,
106
+ scopes,
107
+ isUser: true,
108
+ source: context.cfg.service,
109
+ createdAt: new Date(record.createdAt),
110
+ ...(record.expiresAt != null ? { expiresAt: new Date(record.expiresAt) } : {}),
111
+ });
112
+ const last = record.lastUsedAt == null ? 0 : new Date(record.lastUsedAt).getTime();
113
+ if (Date.now() - last > touchInterval) {
114
+ // Fire and forget: a usage timestamp is never a reason to fail a request, and awaiting it
115
+ // would put a database write on the critical path of every authenticated call.
116
+ void tokens.save({ ...record, lastUsedAt: new Date() })
117
+ .catch((e) => console.error('auth-token: lastUsedAt', e));
118
+ }
119
+ return true;
120
+ },
121
+ /** Server-side guard: it verifies what arrives, it never produces a credential. */
122
+ authenticated: async () => null,
123
+ });
124
+ return service;
125
+ };
126
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare const listAccessTokens: import("@owlmeans/server-entrypoint").RefedEntrypointHandler<import("@owlmeans/entrypoint").AbstractResponse<any>>;
2
+ export declare const createAccessToken: import("@owlmeans/server-entrypoint").RefedEntrypointHandler<import("@owlmeans/entrypoint").AbstractResponse<any>>;
3
+ export declare const revokeAccessToken: import("@owlmeans/server-entrypoint").RefedEntrypointHandler<import("@owlmeans/entrypoint").AbstractResponse<any>>;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAsCA,eAAO,MAAM,gBAAgB,oHAa3B,CAAA;AAEF,eAAO,MAAM,iBAAiB,oHAsC5B,CAAA;AAEF,eAAO,MAAM,iBAAiB,oHAqB5B,CAAA"}
@@ -0,0 +1,94 @@
1
+ import { AuthForbidden, AuthroizationType, ALL_SCOPES, AuthRole } from '@owlmeans/auth';
2
+ import { AUTH_TOKEN_MAX_TTL, AUTH_TOKEN_RESOURCE } from '@owlmeans/auth-token';
3
+ import { requireEntityKey } from '@owlmeans/auth-common';
4
+ import { handleBody, handleParams, handleRequest } from '@owlmeans/server-api';
5
+ import { mintAccessToken } from '../hash.js';
6
+ import { prefixOf } from '../guard.js';
7
+ const view = (record) => {
8
+ const { hash, ...rest } = record;
9
+ return rest;
10
+ };
11
+ const tokens = (ctx) => ctx.resource(AUTH_TOKEN_RESOURCE);
12
+ /**
13
+ * A token may never mint another token.
14
+ *
15
+ * Minting is the one operation that turns a stolen credential into a permanent one: a token that
16
+ * can create tokens survives the revocation of the token that leaked. Creating and revoking
17
+ * therefore require a credential a person produced in a browser — which is also why the deployment
18
+ * names these routes in the guard's deny list, so the refusal is a 401 at the boundary rather than
19
+ * a check every future handler has to remember.
20
+ */
21
+ const refuseTokenAuth = (req, what) => {
22
+ if (req.auth?.type === AuthroizationType.AuthToken) {
23
+ throw new AuthForbidden(what);
24
+ }
25
+ };
26
+ export const listAccessTokens = handleRequest(async (req, context) => {
27
+ const ctx = context;
28
+ const entityId = requireEntityKey(req);
29
+ const profileId = req.auth?.profileId;
30
+ if (profileId == null)
31
+ throw new AuthForbidden('profile');
32
+ const result = await tokens(ctx).list({ entityId, profileId }, { size: 0 });
33
+ return {
34
+ items: result.items
35
+ .map(view)
36
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
37
+ };
38
+ });
39
+ export const createAccessToken = handleBody(async (payload, context, req) => {
40
+ const ctx = context;
41
+ refuseTokenAuth(req, 'token-mint');
42
+ const entityId = requireEntityKey(req);
43
+ const profileId = req.auth?.profileId;
44
+ const userId = req.auth?.userId;
45
+ if (profileId == null || userId == null)
46
+ throw new AuthForbidden('profile');
47
+ const own = req.auth?.scopes ?? [];
48
+ const scopes = payload.scopes ?? own;
49
+ // A token is a delegation, so it can only ever narrow. Asking for a scope the caller does not
50
+ // hold is refused rather than silently dropped: a token that quietly grants less than it was
51
+ // asked for fails later, somewhere else, with an error about the wrong thing.
52
+ if (!own.includes(ALL_SCOPES) && scopes.some(scope => !own.includes(scope))) {
53
+ throw new AuthForbidden('scope');
54
+ }
55
+ // The SAME resolver the guard uses, so a minted token is one this deployment's guard claims.
56
+ const minted = mintAccessToken(prefixOf(ctx));
57
+ const now = new Date();
58
+ const record = await tokens(ctx).create({
59
+ hash: minted.hash,
60
+ display: minted.display,
61
+ name: payload.name,
62
+ userId,
63
+ profileId,
64
+ entityId,
65
+ scopes,
66
+ role: req.auth?.role ?? AuthRole.User,
67
+ createdAt: now,
68
+ ...(payload.expiresIn != null
69
+ ? { expiresAt: new Date(now.getTime() + Math.min(payload.expiresIn * 1000, AUTH_TOKEN_MAX_TTL)) }
70
+ : {}),
71
+ });
72
+ // The only moment the plaintext exists outside the caller's own machine.
73
+ return { token: minted.token, record: view(record) };
74
+ });
75
+ export const revokeAccessToken = handleParams(async (payload, context, req) => {
76
+ const ctx = context;
77
+ refuseTokenAuth(req, 'token-revoke');
78
+ const entityId = requireEntityKey(req);
79
+ const profileId = req.auth?.profileId;
80
+ if (profileId == null)
81
+ throw new AuthForbidden('profile');
82
+ const record = await tokens(ctx).load(payload.id);
83
+ // A token of another profile answers exactly as an unknown id does — an owner learns nothing
84
+ // about tokens that are not theirs, not even that one exists.
85
+ if (record == null || record.entityId !== entityId || record.profileId !== profileId) {
86
+ throw new AuthForbidden('token');
87
+ }
88
+ // Idempotent: revoking twice is what a retry looks like.
89
+ if (record.revokedAt == null) {
90
+ await tokens(ctx).save({ ...record, revokedAt: new Date(), updatedAt: new Date() });
91
+ }
92
+ return { id: payload.id };
93
+ });
94
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The stored form of a token.
3
+ *
4
+ * SHA-256 rather than a password hash on purpose: the input is 192 bits of machine-generated
5
+ * randomness, so there is no dictionary to slow down, and the hash is computed on every single
6
+ * API request — a deliberately slow function here would be a deliberate rate limit on the
7
+ * deployment's whole authenticated surface.
8
+ */
9
+ export declare const hashAccessToken: (token: string) => string;
10
+ export interface MintedToken {
11
+ token: string;
12
+ hash: string;
13
+ display: string;
14
+ }
15
+ /** Mint one token: the plaintext (returned once), its hash (stored) and its display form. */
16
+ export declare const mintAccessToken: (prefix: string) => MintedToken;
17
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,UAAW,MAAM,KAAG,MAA6D,CAAA;AAE7G,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,6FAA6F;AAC7F,eAAO,MAAM,eAAe,WAAY,MAAM,KAAG,WAIhD,CAAA"}
package/build/hash.js ADDED
@@ -0,0 +1,19 @@
1
+ import { AUTH_TOKEN_SECRET_BYTES, displayOf } from '@owlmeans/auth-token';
2
+ import { base58 } from '@scure/base';
3
+ import { sha256 } from '@noble/hashes/sha256';
4
+ import { bytesToHex, randomBytes } from '@noble/hashes/utils';
5
+ /**
6
+ * The stored form of a token.
7
+ *
8
+ * SHA-256 rather than a password hash on purpose: the input is 192 bits of machine-generated
9
+ * randomness, so there is no dictionary to slow down, and the hash is computed on every single
10
+ * API request — a deliberately slow function here would be a deliberate rate limit on the
11
+ * deployment's whole authenticated surface.
12
+ */
13
+ export const hashAccessToken = (token) => bytesToHex(sha256(new TextEncoder().encode(token)));
14
+ /** Mint one token: the plaintext (returned once), its hash (stored) and its display form. */
15
+ export const mintAccessToken = (prefix) => {
16
+ const token = `${prefix}${base58.encode(randomBytes(AUTH_TOKEN_SECRET_BYTES))}`;
17
+ return { token, hash: hashAccessToken(token), display: displayOf(token, prefix) };
18
+ };
19
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AACzE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAE7D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAQ7G,6FAA6F;AAC7F,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAc,EAAe,EAAE;IAC7D,MAAM,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,uBAAuB,CAAC,CAAC,EAAE,CAAA;IAE/E,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,eAAe,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAA;AACnF,CAAC,CAAA"}
@@ -0,0 +1,9 @@
1
+ export * from './consts.js';
2
+ export * from './resource.js';
3
+ export * from './guard.js';
4
+ export * from './hash.js';
5
+ export * from './coguard.js';
6
+ export * from './append.js';
7
+ export * from './handlers/index.js';
8
+ export type * from './types.js';
9
+ //# sourceMappingURL=index.d.ts.map