@owlmeans/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.
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/auth-token",
4
+ "version": "0.1.18-rc.1",
5
+ "generatedAt": "2026-09-10T18:54:42.500Z",
6
+ "canonicalRepo": "https://github.com/owlmeans/common",
7
+ "entries": [
8
+ {
9
+ "kind": "skill",
10
+ "name": "auth-token",
11
+ "category": "package-specific",
12
+ "file": "skills/auth-token/SKILL.md",
13
+ "canonicalPath": ".agents/skills/auth-token/SKILL.md"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: auth-token
3
+ description: How to use @owlmeans/auth-token — the contracts behind long-lived access tokens (API keys) — the token format and its deployment prefix, the three management routes, the Authorization parsing that Bearer needs, and the client-side carrier guard a CLI or an MCP server authenticates with. Auto-invoked when importing the token entrypoints, the carrier guard, parseAuthorizationHeader, or an access-token type.
4
+ user-invocable: false
5
+ ---
6
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
+
8
+ # @owlmeans/auth-token
9
+
10
+ **Layer:** Auth shared
11
+ **Install:** `"@owlmeans/auth-token": "^0.1.18-rc.1"` in `dependencies`
12
+
13
+ The contract half of long-lived access tokens: the record shape, the route declarations, the
14
+ format helpers, and one client-side guard that presents a token it was handed. The server half —
15
+ the store, the verifying guard and the handlers — is `@owlmeans/server-auth-token`; the management
16
+ UI is `@owlmeans/web-auth-token`.
17
+
18
+ ## Key Exports
19
+
20
+ | Export | Description |
21
+ |--------|-------------|
22
+ | `makeAuthTokenEntrypoints(opts?)` | The three routes (`list` GET, `create` POST, `revoke` DELETE `/:id`) under a `/tokens` base. `opts`: `parent`, `path`, `guard` |
23
+ | `makeTokenCarrierGuard(alias, opts)` | A client `GuardService` that presents one token. `opts.token` is a value or a thunk; `opts.scheme` is `'auth-token'` (default) or `'bearer'` |
24
+ | `parseAuthorizationHeader(header)` | `{ scheme, value }` with the scheme lower-cased, or `null` |
25
+ | `isAccessToken(value, prefix)` · `displayOf(token, prefix)` | Whether a value is one of this deployment's tokens; the half of it that may be shown again |
26
+ | `CreateAccessTokenSchema` · `AccessTokenParamsSchema` | The ajv body/params filters |
27
+ | `authToken` | `{ base, list, create, revoke }` route aliases |
28
+ | `GUARD_AUTH_TOKEN` · `AUTH_TOKEN_RESOURCE` · `AUTH_TOKEN_COLLECTION` | `'guard:auth-token'`, `'auth-token:token'`, `'access-token'` |
29
+ | `AUTH_TOKEN_DEFAULT_PREFIX` | `'owl_'` — a deployment overrides it |
30
+ | `AUTH_TOKEN_SECRET_BYTES` (24) · `AUTH_TOKEN_DISPLAY_LENGTH` (8) | 192 bits of randomness; 8 characters kept for display |
31
+ | `AUTH_TOKEN_TOUCH_INTERVAL` (5 min) · `AUTH_TOKEN_MAX_TTL` (366 d) · `AUTH_TOKEN_NAME_MAX` (64) | Tuning |
32
+ | `AUTH_TOKEN_SCHEME` · `BEARER_SCHEME` | `'auth-token'`, `'bearer'` — lower-cased, for comparison |
33
+ | `AccessTokenRecord`, `AccessTokenView`, `CreateAccessToken`, `IssuedAccessToken`, `AccessTokenList`, `TokenCarrierOptions`, `AuthTokenEntrypointOptions` | Types |
34
+
35
+ ## The prefix is what makes a token CLAIMABLE
36
+
37
+ A token is `<prefix><base58(24 random bytes)>`. The prefix is per deployment (`vib_`, `acme_`), and
38
+ the guard answers `match` only for a value that starts with it — so an access token and an Ed25519
39
+ session bearer arrive under the same `Authorization` header without either guard shadowing the
40
+ other, and two deployments never mistake each other's credentials for their own.
41
+
42
+ The plaintext exists exactly once, in the create response. What is stored is its hash; what a list
43
+ shows forever after is `display` — the prefix plus 8 characters, enough to tell two of your own
44
+ tokens apart and far too little to replay.
45
+
46
+ ## Both `AUTH-TOKEN` and `Bearer` are accepted, and that is why this package parses headers itself
47
+
48
+ A third-party client configured with a URL sends `Bearer` whatever the documentation says. But
49
+ `extractAuthToken` (`@owlmeans/auth-common`) compares the prefix against `type.toUpperCase()`, so it
50
+ matches `AUTH-TOKEN` and can **never** match `Bearer`. `parseAuthorizationHeader` is the
51
+ replacement: it lower-cases the scheme so a caller compares once, takes the first of several headers
52
+ a proxy folded together, keeps a value that itself contains spaces, and answers `null` for a scheme
53
+ with no value behind it.
54
+
55
+ ## The carrier guard is how a process with no browser authenticates
56
+
57
+ `authMiddleware` asks every guard an entrypoint declares for `authenticated(req)` and stamps the
58
+ first non-null answer onto the header — so registering the carrier under the alias the routes
59
+ already name (`DEFAULT_GUARD`, usually) makes an **unchanged route declaration** work from a CLI, an
60
+ MCP server or a test.
61
+
62
+ ```typescript
63
+ context.registerService(makeTokenCarrierGuard(DEFAULT_GUARD, { token, scheme: 'auth-token' }))
64
+ context.registerMiddleware(authMiddleware)
65
+ ```
66
+
67
+ There is no session, no refresh and no storage: the token is a long-lived credential the caller was
68
+ handed. `opts.token` may be a thunk because a long-running process reads it from an environment
69
+ variable and must not cache it past a reconfiguration.
70
+
71
+ ## The management surface is deliberately not a CRUD
72
+
73
+ There is no update. A token's scopes and lifetime are fixed at issuance, because a token that can be
74
+ widened later is a grant nobody can reason about from the moment it was created.
75
+
76
+ ```typescript
77
+ context.registerEntrypoints(makeAuthTokenEntrypoints({ parent: account.base, path: '/tokens' }))
78
+ ```
79
+
80
+ Mount it under an account section: the ownership gate that already guards a person's own settings
81
+ then guards the credentials that speak for them. A base with a parent inherits its guard and gate; a
82
+ base without one carries `opts.guard`.
83
+
84
+ `CreateAccessToken.expiresIn` is **seconds** (the server clamps it to `AUTH_TOKEN_MAX_TTL`), while a
85
+ UI usually offers days — convert at the form, and omit the field entirely for "never" rather than
86
+ sending a zero.
87
+
88
+ ## Depends On
89
+
90
+ - `@owlmeans/auth` — `AuthRole`, the `Authorization` header name
91
+ - `@owlmeans/context`, `@owlmeans/entrypoint`, `@owlmeans/resource`, `@owlmeans/route`
92
+
93
+ ## Related
94
+
95
+ - [[server-auth-token]] — the store, the verifying guard, the handlers and the coguard
96
+ - [[web-auth-token]] — the management panel and its hook
97
+ - [[auth-protocol]] — where long-lived tokens sit among the other authentication paths
98
+ - [[auth-common]] — `authMiddleware`, `DEFAULT_GUARD`, `extractAuthToken`
@@ -0,0 +1,16 @@
1
+ import type { GuardService } from '@owlmeans/entrypoint';
2
+ import type { TokenCarrierOptions } from './types.js';
3
+ /**
4
+ * A client-side guard that carries one access token.
5
+ *
6
+ * `authMiddleware` asks every guard an entrypoint declares for `authenticated(req)` and stamps the
7
+ * first non-null answer onto the `Authorization` header — so registering this under the alias the
8
+ * routes already name (`DEFAULT_GUARD`, usually) makes an unchanged route declaration work for a
9
+ * non-browser client. There is no session, no refresh and no storage: the token is a long-lived
10
+ * credential the caller was handed, and this is the object that presents it.
11
+ *
12
+ * The token may be a value or a thunk, because a CLI reads it from an environment variable that a
13
+ * long-running process should not cache past a reconfiguration.
14
+ */
15
+ export declare const makeTokenCarrierGuard: (alias: string, opts: TokenCarrierOptions) => GuardService;
16
+ //# sourceMappingURL=carrier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carrier.d.ts","sourceRoot":"","sources":["../src/carrier.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,UAAW,MAAM,QAAQ,mBAAmB,KAAG,YAsBhF,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { createService } from '@owlmeans/context';
2
+ import { AUTH_TOKEN_SCHEME, BEARER_SCHEME } from './consts.js';
3
+ /**
4
+ * A client-side guard that carries one access token.
5
+ *
6
+ * `authMiddleware` asks every guard an entrypoint declares for `authenticated(req)` and stamps the
7
+ * first non-null answer onto the `Authorization` header — so registering this under the alias the
8
+ * routes already name (`DEFAULT_GUARD`, usually) makes an unchanged route declaration work for a
9
+ * non-browser client. There is no session, no refresh and no storage: the token is a long-lived
10
+ * credential the caller was handed, and this is the object that presents it.
11
+ *
12
+ * The token may be a value or a thunk, because a CLI reads it from an environment variable that a
13
+ * long-running process should not cache past a reconfiguration.
14
+ */
15
+ export const makeTokenCarrierGuard = (alias, opts) => {
16
+ const scheme = opts.scheme === BEARER_SCHEME ? 'Bearer' : AUTH_TOKEN_SCHEME.toUpperCase();
17
+ const resolve = async () => {
18
+ const token = typeof opts.token === 'function' ? await opts.token() : opts.token;
19
+ return token == null || token === '' ? null : token;
20
+ };
21
+ const service = createService(alias, {
22
+ match: async () => await resolve() != null,
23
+ handle: async () => void 0,
24
+ authenticated: async () => {
25
+ const token = await resolve();
26
+ return token == null ? null : `${scheme} ${token}`;
27
+ },
28
+ }, service => async () => { service.initialized = true; });
29
+ return service;
30
+ };
31
+ //# sourceMappingURL=carrier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carrier.js","sourceRoot":"","sources":["../src/carrier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAG9D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAa,EAAE,IAAyB,EAAgB,EAAE;IAC9F,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,WAAW,EAAE,CAAA;IAEzF,MAAM,OAAO,GAAG,KAAK,IAA4B,EAAE;QACjD,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAA;QAEhF,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAA;IACrD,CAAC,CAAA;IAED,MAAM,OAAO,GAAiB,aAAa,CAAe,KAAK,EAAE;QAC/D,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,OAAO,EAAE,IAAI,IAAI;QAE1C,MAAM,EAAE,KAAK,IAAO,EAAE,CAAC,KAAK,CAAM;QAElC,aAAa,EAAE,KAAK,IAAI,EAAE;YACxB,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAA;YAE7B,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAA;QACpD,CAAC;KACF,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAAA;IAEzD,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
@@ -0,0 +1,52 @@
1
+ /** The guard alias a long-lived access token is verified by. */
2
+ export declare const GUARD_AUTH_TOKEN = "guard:auth-token";
3
+ /** The resource alias holding access-token records. */
4
+ export declare const AUTH_TOKEN_RESOURCE = "auth-token:token";
5
+ /** The collection an access-token resource is backed by, where the backend has collections. */
6
+ export declare const AUTH_TOKEN_COLLECTION = "access-token";
7
+ /**
8
+ * The default prefix every issued token carries.
9
+ *
10
+ * The prefix is what makes a token CLAIMABLE: the guard answers `match` only for a value that
11
+ * starts with it, so an access token and a session bearer can share the `Bearer` scheme without
12
+ * either guard shadowing the other. A deployment overrides it with its own — `vib_`, `acme_` — and
13
+ * two deployments then never mistake each other's credentials for their own.
14
+ */
15
+ export declare const AUTH_TOKEN_DEFAULT_PREFIX = "owl_";
16
+ /** Random bytes behind one token. 24 bytes ≈ 192 bits, base58-encoded to 33 characters. */
17
+ export declare const AUTH_TOKEN_SECRET_BYTES = 24;
18
+ /**
19
+ * How many characters of a token are kept for display.
20
+ *
21
+ * The plaintext is shown once, at creation, and never again — what a list shows is the prefix plus
22
+ * this many characters, which is enough for a person to tell two of their own tokens apart and far
23
+ * too little to use.
24
+ */
25
+ export declare const AUTH_TOKEN_DISPLAY_LENGTH = 8;
26
+ /**
27
+ * How often a token's `lastUsedAt` is written.
28
+ *
29
+ * Every request would mean a database write per API call for a field nobody reads in real time.
30
+ * Five minutes answers the only question the field exists for — "is this token still in use?" —
31
+ * at a thousandth of the cost.
32
+ */
33
+ export declare const AUTH_TOKEN_TOUCH_INTERVAL: number;
34
+ /** The longest lifetime a token may be issued for. */
35
+ export declare const AUTH_TOKEN_MAX_TTL: number;
36
+ export declare const AUTH_TOKEN_NAME_MAX = 64;
37
+ /**
38
+ * The route aliases of the token-management surface.
39
+ *
40
+ * Declared here so a client and a server address the same names, and mounted under whatever parent
41
+ * the application chooses — an account section, a settings section, a dedicated prefix.
42
+ */
43
+ export declare const authToken: Readonly<{
44
+ base: "auth-token:base";
45
+ list: "auth-token:list";
46
+ create: "auth-token:create";
47
+ revoke: "auth-token:revoke";
48
+ }>;
49
+ /** The `Authorization` schemes a token may arrive under, lower-cased. */
50
+ export declare const AUTH_TOKEN_SCHEME = "auth-token";
51
+ export declare const BEARER_SCHEME = "bearer";
52
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,qBAAqB,CAAA;AAElD,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,qBAAqB,CAAA;AAErD,+FAA+F;AAC/F,eAAO,MAAM,qBAAqB,iBAAiB,CAAA;AAEnD;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,SAAS,CAAA;AAE/C,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,QAAgB,CAAA;AAEtD,sDAAsD;AACtD,eAAO,MAAM,kBAAkB,QAA4B,CAAA;AAE3D,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;;;;EAKpB,CAAA;AAEF,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,eAAe,CAAA;AAC7C,eAAO,MAAM,aAAa,WAAW,CAAA"}
@@ -0,0 +1,52 @@
1
+ /** The guard alias a long-lived access token is verified by. */
2
+ export const GUARD_AUTH_TOKEN = 'guard:auth-token';
3
+ /** The resource alias holding access-token records. */
4
+ export const AUTH_TOKEN_RESOURCE = 'auth-token:token';
5
+ /** The collection an access-token resource is backed by, where the backend has collections. */
6
+ export const AUTH_TOKEN_COLLECTION = 'access-token';
7
+ /**
8
+ * The default prefix every issued token carries.
9
+ *
10
+ * The prefix is what makes a token CLAIMABLE: the guard answers `match` only for a value that
11
+ * starts with it, so an access token and a session bearer can share the `Bearer` scheme without
12
+ * either guard shadowing the other. A deployment overrides it with its own — `vib_`, `acme_` — and
13
+ * two deployments then never mistake each other's credentials for their own.
14
+ */
15
+ export const AUTH_TOKEN_DEFAULT_PREFIX = 'owl_';
16
+ /** Random bytes behind one token. 24 bytes ≈ 192 bits, base58-encoded to 33 characters. */
17
+ export const AUTH_TOKEN_SECRET_BYTES = 24;
18
+ /**
19
+ * How many characters of a token are kept for display.
20
+ *
21
+ * The plaintext is shown once, at creation, and never again — what a list shows is the prefix plus
22
+ * this many characters, which is enough for a person to tell two of their own tokens apart and far
23
+ * too little to use.
24
+ */
25
+ export const AUTH_TOKEN_DISPLAY_LENGTH = 8;
26
+ /**
27
+ * How often a token's `lastUsedAt` is written.
28
+ *
29
+ * Every request would mean a database write per API call for a field nobody reads in real time.
30
+ * Five minutes answers the only question the field exists for — "is this token still in use?" —
31
+ * at a thousandth of the cost.
32
+ */
33
+ export const AUTH_TOKEN_TOUCH_INTERVAL = 5 * 60 * 1000;
34
+ /** The longest lifetime a token may be issued for. */
35
+ export const AUTH_TOKEN_MAX_TTL = 366 * 24 * 60 * 60 * 1000;
36
+ export const AUTH_TOKEN_NAME_MAX = 64;
37
+ /**
38
+ * The route aliases of the token-management surface.
39
+ *
40
+ * Declared here so a client and a server address the same names, and mounted under whatever parent
41
+ * the application chooses — an account section, a settings section, a dedicated prefix.
42
+ */
43
+ export const authToken = Object.freeze({
44
+ base: 'auth-token:base',
45
+ list: 'auth-token:list',
46
+ create: 'auth-token:create',
47
+ revoke: 'auth-token:revoke',
48
+ });
49
+ /** The `Authorization` schemes a token may arrive under, lower-cased. */
50
+ export const AUTH_TOKEN_SCHEME = 'auth-token';
51
+ export const BEARER_SCHEME = 'bearer';
52
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAA;AAElD,uDAAuD;AACvD,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AAErD,+FAA+F;AAC/F,MAAM,CAAC,MAAM,qBAAqB,GAAG,cAAc,CAAA;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAA;AAE/C,2FAA2F;AAC3F,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAA;AAEzC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAA;AAE1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AAEtD,sDAAsD;AACtD,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAE3D,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAErC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,iBAAiB;IACvB,IAAI,EAAE,iBAAiB;IACvB,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,mBAAmB;CAC5B,CAAC,CAAA;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAA;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,CAAA"}
@@ -0,0 +1,15 @@
1
+ import type { CommonEntrypoint } from '@owlmeans/entrypoint';
2
+ import type { AuthTokenEntrypointOptions } from './types.js';
3
+ /**
4
+ * The three routes a token surface needs, ready to be spread into an application's entrypoints.
5
+ *
6
+ * Mounted under whatever parent the application chooses — an account section usually, so the
7
+ * ownership gate that already protects a person's own settings protects their tokens too. A base
8
+ * with a parent inherits its guard and its gate; a base without one carries `opts.guard`.
9
+ *
10
+ * Deliberately not a resource CRUD: there is no update. A token's scopes and lifetime are fixed at
11
+ * issuance, because a token that can be widened later is a token whose grant nobody can reason
12
+ * about from the moment it was created.
13
+ */
14
+ export declare const makeAuthTokenEntrypoints: (opts?: AuthTokenEntrypointOptions) => CommonEntrypoint[];
15
+ //# sourceMappingURL=entrypoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../src/entrypoints.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAI5D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAA;AAE5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,UAC7B,0BAA0B,KAC/B,gBAAgB,EAoBlB,CAAA"}
@@ -0,0 +1,30 @@
1
+ import { body, entrypoint, filter, guard, params } from '@owlmeans/entrypoint';
2
+ import { route, RouteMethod } from '@owlmeans/route';
3
+ import { authToken } from './consts.js';
4
+ import { AccessTokenParamsSchema, CreateAccessTokenSchema } from './schemas.js';
5
+ /**
6
+ * The three routes a token surface needs, ready to be spread into an application's entrypoints.
7
+ *
8
+ * Mounted under whatever parent the application chooses — an account section usually, so the
9
+ * ownership gate that already protects a person's own settings protects their tokens too. A base
10
+ * with a parent inherits its guard and its gate; a base without one carries `opts.guard`.
11
+ *
12
+ * Deliberately not a resource CRUD: there is no update. A token's scopes and lifetime are fixed at
13
+ * issuance, because a token that can be widened later is a token whose grant nobody can reason
14
+ * about from the moment it was created.
15
+ */
16
+ export const makeAuthTokenEntrypoints = (opts = {}) => {
17
+ const path = opts.path ?? '/tokens';
18
+ const base = opts.parent != null
19
+ ? entrypoint(route(authToken.base, path, { parent: opts.parent }))
20
+ : entrypoint(route(authToken.base, path), opts.guard != null ? guard(opts.guard) : undefined);
21
+ return [
22
+ base,
23
+ entrypoint(route(authToken.list, '/', {
24
+ parent: authToken.base, method: RouteMethod.GET
25
+ })),
26
+ entrypoint(route(authToken.create, '/', { parent: authToken.base, method: RouteMethod.POST }), filter(body(CreateAccessTokenSchema))),
27
+ entrypoint(route(authToken.revoke, '/:id', { parent: authToken.base, method: RouteMethod.DELETE }), filter(params(AccessTokenParamsSchema))),
28
+ ];
29
+ };
30
+ //# sourceMappingURL=entrypoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entrypoints.js","sourceRoot":"","sources":["../src/entrypoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAE9E,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAG/E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACtC,IAAI,GAA+B,EAAE,EACjB,EAAE;IACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,SAAS,CAAA;IACnC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI;QAC9B,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAClE,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAE/F,OAAO;QACL,IAAI;QACJ,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,EAAE;YACpC,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,GAAG;SAChD,CAAC,CAAC;QACH,UAAU,CACR,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC,EAClF,MAAM,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CACtC;QACD,UAAU,CACR,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC,EACvF,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,CACxC;KACF,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Split an `Authorization` header into its scheme and its value.
3
+ *
4
+ * Deliberately NOT `extractAuthToken` from `@owlmeans/auth-common`: that compares the prefix
5
+ * against `type.toUpperCase()`, so it matches `AUTH-TOKEN` and can never match `Bearer` — the
6
+ * exact spelling every third-party client sends. The scheme comes back lower-cased so a caller
7
+ * compares once.
8
+ */
9
+ export declare const parseAuthorizationHeader: (header: string | string[] | undefined) => {
10
+ scheme: string;
11
+ value: string;
12
+ } | null;
13
+ /** Whether a value looks like an access token this deployment issued. */
14
+ export declare const isAccessToken: (value: string | null | undefined, prefix: string) => boolean;
15
+ /**
16
+ * The half of a token that may be shown again.
17
+ *
18
+ * Prefix plus the first characters of the secret. Enough to tell two tokens apart in a list, and
19
+ * far short of anything that could be replayed.
20
+ */
21
+ export declare const displayOf: (token: string, prefix: string) => string;
22
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,WAC3B,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,KACpC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAatC,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,aAAa,UAAW,MAAM,GAAG,IAAI,GAAG,SAAS,UAAU,MAAM,KAAG,OACN,CAAA;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,SAAS,UAAW,MAAM,UAAU,MAAM,KAAG,MAIzD,CAAA"}
@@ -0,0 +1,36 @@
1
+ import { AUTH_TOKEN_DISPLAY_LENGTH } from './consts.js';
2
+ /**
3
+ * Split an `Authorization` header into its scheme and its value.
4
+ *
5
+ * Deliberately NOT `extractAuthToken` from `@owlmeans/auth-common`: that compares the prefix
6
+ * against `type.toUpperCase()`, so it matches `AUTH-TOKEN` and can never match `Bearer` — the
7
+ * exact spelling every third-party client sends. The scheme comes back lower-cased so a caller
8
+ * compares once.
9
+ */
10
+ export const parseAuthorizationHeader = (header) => {
11
+ const raw = Array.isArray(header) ? header[0] : header;
12
+ if (raw == null)
13
+ return null;
14
+ const trimmed = raw.trim();
15
+ const space = trimmed.indexOf(' ');
16
+ if (space < 1)
17
+ return null;
18
+ const scheme = trimmed.slice(0, space).toLowerCase();
19
+ const value = trimmed.slice(space + 1).trim();
20
+ if (value.length < 1)
21
+ return null;
22
+ return { scheme, value };
23
+ };
24
+ /** Whether a value looks like an access token this deployment issued. */
25
+ export const isAccessToken = (value, prefix) => value != null && value.length > prefix.length && value.startsWith(prefix);
26
+ /**
27
+ * The half of a token that may be shown again.
28
+ *
29
+ * Prefix plus the first characters of the secret. Enough to tell two tokens apart in a list, and
30
+ * far short of anything that could be replayed.
31
+ */
32
+ export const displayOf = (token, prefix) => {
33
+ const secret = token.startsWith(prefix) ? token.slice(prefix.length) : token;
34
+ return `${prefix}${secret.slice(0, AUTH_TOKEN_DISPLAY_LENGTH)}`;
35
+ };
36
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAEvD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACtC,MAAqC,EACK,EAAE;IAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IACtD,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IAE5B,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAClC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAE1B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;IACpD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAC7C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AAC1B,CAAC,CAAA;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAgC,EAAE,MAAc,EAAW,EAAE,CACzF,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AAE3E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAa,EAAE,MAAc,EAAU,EAAE;IACjE,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IAE5E,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,yBAAyB,CAAC,EAAE,CAAA;AACjE,CAAC,CAAA"}
@@ -0,0 +1,7 @@
1
+ export * from './consts.js';
2
+ export * from './schemas.js';
3
+ export * from './entrypoints.js';
4
+ export * from './format.js';
5
+ export * from './carrier.js';
6
+ export type * from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,mBAAmB,YAAY,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './consts.js';
2
+ export * from './schemas.js';
3
+ export * from './entrypoints.js';
4
+ export * from './format.js';
5
+ export * from './carrier.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,5 @@
1
+ import type { JSONSchemaType } from 'ajv';
2
+ import type { AccessTokenParams, CreateAccessToken } from './types.js';
3
+ export declare const CreateAccessTokenSchema: JSONSchemaType<CreateAccessToken>;
4
+ export declare const AccessTokenParamsSchema: JSONSchemaType<AccessTokenParams>;
5
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,KAAK,CAAA;AAEzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEtE,eAAO,MAAM,uBAAuB,EAAE,cAAc,CAAC,iBAAiB,CAmBrE,CAAA;AAED,eAAO,MAAM,uBAAuB,EAAE,cAAc,CAAC,iBAAiB,CAKrE,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { AUTH_TOKEN_MAX_TTL, AUTH_TOKEN_NAME_MAX } from './consts.js';
2
+ export const CreateAccessTokenSchema = {
3
+ type: 'object',
4
+ properties: {
5
+ name: { type: 'string', minLength: 1, maxLength: AUTH_TOKEN_NAME_MAX },
6
+ scopes: {
7
+ type: 'array',
8
+ nullable: true,
9
+ maxItems: 32,
10
+ items: { type: 'string', minLength: 1, maxLength: 128 },
11
+ },
12
+ expiresIn: {
13
+ type: 'integer',
14
+ nullable: true,
15
+ minimum: 60,
16
+ maximum: Math.floor(AUTH_TOKEN_MAX_TTL / 1000),
17
+ },
18
+ },
19
+ required: ['name'],
20
+ additionalProperties: false,
21
+ };
22
+ export const AccessTokenParamsSchema = {
23
+ type: 'object',
24
+ properties: { id: { type: 'string', minLength: 1, maxLength: 128 } },
25
+ required: ['id'],
26
+ additionalProperties: false,
27
+ };
28
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAGrE,MAAM,CAAC,MAAM,uBAAuB,GAAsC;IACxE,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,mBAAmB,EAAE;QACtE,MAAM,EAAE;YACN,IAAI,EAAE,OAAO;YACb,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,EAAE;YACZ,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE;SACxD;QACD,SAAS,EAAE;YACT,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,kBAAkB,GAAG,IAAI,CAAC;SAC/C;KACF;IACD,QAAQ,EAAE,CAAC,MAAM,CAAC;IAClB,oBAAoB,EAAE,KAAK;CAC5B,CAAA;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAsC;IACxE,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE;IACpE,QAAQ,EAAE,CAAC,IAAI,CAAC;IAChB,oBAAoB,EAAE,KAAK;CAC5B,CAAA"}
@@ -0,0 +1,72 @@
1
+ import type { AuthRole } from '@owlmeans/auth';
2
+ import type { ResourceRecord } from '@owlmeans/resource';
3
+ /**
4
+ * One long-lived access token, as stored.
5
+ *
6
+ * The plaintext is never here. What is stored is its hash — a stolen database yields no usable
7
+ * credential — plus `display`, the prefix and a few characters, which is what a person sees in a
8
+ * list. A token is bound to the profile that minted it and can never outrank it: the guard
9
+ * intersects its scopes with the profile's on every request, so revoking a profile's access
10
+ * revokes every token it ever issued without touching a single token record.
11
+ */
12
+ export interface AccessTokenRecord extends ResourceRecord {
13
+ id?: string;
14
+ /** SHA-256 of the plaintext, hex. The only copy of the secret that exists after issuance. */
15
+ hash: string;
16
+ /** Prefix + the first characters of the secret. Shown in lists; useless as a credential. */
17
+ display: string;
18
+ /** What the owner called it. */
19
+ name: string;
20
+ userId: string;
21
+ profileId: string;
22
+ entityId: string;
23
+ scopes: string[];
24
+ role: AuthRole;
25
+ createdAt: Date;
26
+ updatedAt?: Date;
27
+ /** Written at most once per touch interval — a usage signal, not an access log. */
28
+ lastUsedAt?: Date;
29
+ expiresAt?: Date;
30
+ /** Set once and never unset. A revoked token is kept so its display name still resolves. */
31
+ revokedAt?: Date;
32
+ }
33
+ /** What a caller may see. The hash never leaves the server. */
34
+ export type AccessTokenView = Omit<AccessTokenRecord, 'hash'>;
35
+ export interface CreateAccessToken {
36
+ name: string;
37
+ /** A subset of the caller's own scopes. Defaults to all of them. */
38
+ scopes?: string[];
39
+ /** Lifetime in seconds. Absent means no expiry; clamped to the maximum TTL. */
40
+ expiresIn?: number;
41
+ }
42
+ /** The one moment the plaintext exists outside the caller's own machine. */
43
+ export interface IssuedAccessToken {
44
+ token: string;
45
+ record: AccessTokenView;
46
+ }
47
+ export interface AccessTokenList {
48
+ items: AccessTokenView[];
49
+ }
50
+ export interface AccessTokenParams {
51
+ id: string;
52
+ }
53
+ /**
54
+ * How a client presents a token it already holds.
55
+ *
56
+ * `auth-token` is the OwlMeans scheme; `bearer` is what a third-party client — an MCP host reading
57
+ * a URL configuration, a curl script — will send whatever the documentation says. Both are
58
+ * accepted on the way in; a client chooses the one its transport is comfortable with.
59
+ */
60
+ export interface TokenCarrierOptions {
61
+ token: string | (() => string | Promise<string>);
62
+ scheme?: 'auth-token' | 'bearer';
63
+ }
64
+ export interface AuthTokenEntrypointOptions {
65
+ /** The entrypoint the token routes hang under. */
66
+ parent?: string;
67
+ /** Path of the token base, relative to the parent. Defaults to `/tokens`. */
68
+ path?: string;
69
+ /** The guard the base carries when it has no parent to inherit one from. */
70
+ guard?: string;
71
+ }
72
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,OAAO,EAAE,MAAM,CAAA;IACf,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,mFAAmF;IACnF,UAAU,CAAC,EAAE,IAAI,CAAA;IACjB,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,IAAI,CAAA;CACjB;AAED,+DAA+D;AAC/D,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,eAAe,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,MAAM,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAA;CACjC;AAED,MAAM,WAAW,0BAA0B;IACzC,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@owlmeans/auth-token",
3
+ "version": "0.1.18-rc.1",
4
+ "license": "MIT",
5
+ "description": "Long-lived access tokens (API keys) on the OwlMeans auth rails — the record shape, the token format, the management entrypoints and the client-side carrier guard.",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc -b",
9
+ "dev": "sleep 2 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
10
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
11
+ "test": "bun test ./tests"
12
+ },
13
+ "main": "build/index.js",
14
+ "module": "build/index.js",
15
+ "types": "build/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./build/index.js",
19
+ "require": "./build/index.js",
20
+ "default": "./build/index.js",
21
+ "module": "./build/index.js",
22
+ "types": "./build/index.d.ts"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "@owlmeans/auth": "^0.1.18-rc.10",
27
+ "@owlmeans/context": "^0.1.18-rc.9",
28
+ "@owlmeans/entrypoint": "^0.1.18-rc.12",
29
+ "@owlmeans/resource": "^0.1.18-rc.10",
30
+ "@owlmeans/route": "^0.1.18-rc.10"
31
+ },
32
+ "devDependencies": {
33
+ "@owlmeans/dep-config": "workspace:*",
34
+ "@types/bun": "^1.4.0",
35
+ "ajv": "^8.17.1",
36
+ "nodemon": "^3.1.14",
37
+ "typescript": "^7.0.2"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }
package/src/carrier.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { createService } from '@owlmeans/context'
2
+ import type { GuardService } from '@owlmeans/entrypoint'
3
+ import { AUTH_TOKEN_SCHEME, BEARER_SCHEME } from './consts.js'
4
+ import type { TokenCarrierOptions } from './types.js'
5
+
6
+ /**
7
+ * A client-side guard that carries one access token.
8
+ *
9
+ * `authMiddleware` asks every guard an entrypoint declares for `authenticated(req)` and stamps the
10
+ * first non-null answer onto the `Authorization` header — so registering this under the alias the
11
+ * routes already name (`DEFAULT_GUARD`, usually) makes an unchanged route declaration work for a
12
+ * non-browser client. There is no session, no refresh and no storage: the token is a long-lived
13
+ * credential the caller was handed, and this is the object that presents it.
14
+ *
15
+ * The token may be a value or a thunk, because a CLI reads it from an environment variable that a
16
+ * long-running process should not cache past a reconfiguration.
17
+ */
18
+ export const makeTokenCarrierGuard = (alias: string, opts: TokenCarrierOptions): GuardService => {
19
+ const scheme = opts.scheme === BEARER_SCHEME ? 'Bearer' : AUTH_TOKEN_SCHEME.toUpperCase()
20
+
21
+ const resolve = async (): Promise<string | null> => {
22
+ const token = typeof opts.token === 'function' ? await opts.token() : opts.token
23
+
24
+ return token == null || token === '' ? null : token
25
+ }
26
+
27
+ const service: GuardService = createService<GuardService>(alias, {
28
+ match: async () => await resolve() != null,
29
+
30
+ handle: async <T>() => void 0 as T,
31
+
32
+ authenticated: async () => {
33
+ const token = await resolve()
34
+
35
+ return token == null ? null : `${scheme} ${token}`
36
+ },
37
+ }, service => async () => { service.initialized = true })
38
+
39
+ return service
40
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,61 @@
1
+ /** The guard alias a long-lived access token is verified by. */
2
+ export const GUARD_AUTH_TOKEN = 'guard:auth-token'
3
+
4
+ /** The resource alias holding access-token records. */
5
+ export const AUTH_TOKEN_RESOURCE = 'auth-token:token'
6
+
7
+ /** The collection an access-token resource is backed by, where the backend has collections. */
8
+ export const AUTH_TOKEN_COLLECTION = 'access-token'
9
+
10
+ /**
11
+ * The default prefix every issued token carries.
12
+ *
13
+ * The prefix is what makes a token CLAIMABLE: the guard answers `match` only for a value that
14
+ * starts with it, so an access token and a session bearer can share the `Bearer` scheme without
15
+ * either guard shadowing the other. A deployment overrides it with its own — `vib_`, `acme_` — and
16
+ * two deployments then never mistake each other's credentials for their own.
17
+ */
18
+ export const AUTH_TOKEN_DEFAULT_PREFIX = 'owl_'
19
+
20
+ /** Random bytes behind one token. 24 bytes ≈ 192 bits, base58-encoded to 33 characters. */
21
+ export const AUTH_TOKEN_SECRET_BYTES = 24
22
+
23
+ /**
24
+ * How many characters of a token are kept for display.
25
+ *
26
+ * The plaintext is shown once, at creation, and never again — what a list shows is the prefix plus
27
+ * this many characters, which is enough for a person to tell two of their own tokens apart and far
28
+ * too little to use.
29
+ */
30
+ export const AUTH_TOKEN_DISPLAY_LENGTH = 8
31
+
32
+ /**
33
+ * How often a token's `lastUsedAt` is written.
34
+ *
35
+ * Every request would mean a database write per API call for a field nobody reads in real time.
36
+ * Five minutes answers the only question the field exists for — "is this token still in use?" —
37
+ * at a thousandth of the cost.
38
+ */
39
+ export const AUTH_TOKEN_TOUCH_INTERVAL = 5 * 60 * 1000
40
+
41
+ /** The longest lifetime a token may be issued for. */
42
+ export const AUTH_TOKEN_MAX_TTL = 366 * 24 * 60 * 60 * 1000
43
+
44
+ export const AUTH_TOKEN_NAME_MAX = 64
45
+
46
+ /**
47
+ * The route aliases of the token-management surface.
48
+ *
49
+ * Declared here so a client and a server address the same names, and mounted under whatever parent
50
+ * the application chooses — an account section, a settings section, a dedicated prefix.
51
+ */
52
+ export const authToken = Object.freeze({
53
+ base: 'auth-token:base',
54
+ list: 'auth-token:list',
55
+ create: 'auth-token:create',
56
+ revoke: 'auth-token:revoke',
57
+ })
58
+
59
+ /** The `Authorization` schemes a token may arrive under, lower-cased. */
60
+ export const AUTH_TOKEN_SCHEME = 'auth-token'
61
+ export const BEARER_SCHEME = 'bearer'
@@ -0,0 +1,41 @@
1
+ import { body, entrypoint, filter, guard, params } from '@owlmeans/entrypoint'
2
+ import type { CommonEntrypoint } from '@owlmeans/entrypoint'
3
+ import { route, RouteMethod } from '@owlmeans/route'
4
+ import { authToken } from './consts.js'
5
+ import { AccessTokenParamsSchema, CreateAccessTokenSchema } from './schemas.js'
6
+ import type { AuthTokenEntrypointOptions } from './types.js'
7
+
8
+ /**
9
+ * The three routes a token surface needs, ready to be spread into an application's entrypoints.
10
+ *
11
+ * Mounted under whatever parent the application chooses — an account section usually, so the
12
+ * ownership gate that already protects a person's own settings protects their tokens too. A base
13
+ * with a parent inherits its guard and its gate; a base without one carries `opts.guard`.
14
+ *
15
+ * Deliberately not a resource CRUD: there is no update. A token's scopes and lifetime are fixed at
16
+ * issuance, because a token that can be widened later is a token whose grant nobody can reason
17
+ * about from the moment it was created.
18
+ */
19
+ export const makeAuthTokenEntrypoints = (
20
+ opts: AuthTokenEntrypointOptions = {}
21
+ ): CommonEntrypoint[] => {
22
+ const path = opts.path ?? '/tokens'
23
+ const base = opts.parent != null
24
+ ? entrypoint(route(authToken.base, path, { parent: opts.parent }))
25
+ : entrypoint(route(authToken.base, path), opts.guard != null ? guard(opts.guard) : undefined)
26
+
27
+ return [
28
+ base,
29
+ entrypoint(route(authToken.list, '/', {
30
+ parent: authToken.base, method: RouteMethod.GET
31
+ })),
32
+ entrypoint(
33
+ route(authToken.create, '/', { parent: authToken.base, method: RouteMethod.POST }),
34
+ filter(body(CreateAccessTokenSchema))
35
+ ),
36
+ entrypoint(
37
+ route(authToken.revoke, '/:id', { parent: authToken.base, method: RouteMethod.DELETE }),
38
+ filter(params(AccessTokenParamsSchema))
39
+ ),
40
+ ]
41
+ }
package/src/format.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { AUTH_TOKEN_DISPLAY_LENGTH } from './consts.js'
2
+
3
+ /**
4
+ * Split an `Authorization` header into its scheme and its value.
5
+ *
6
+ * Deliberately NOT `extractAuthToken` from `@owlmeans/auth-common`: that compares the prefix
7
+ * against `type.toUpperCase()`, so it matches `AUTH-TOKEN` and can never match `Bearer` — the
8
+ * exact spelling every third-party client sends. The scheme comes back lower-cased so a caller
9
+ * compares once.
10
+ */
11
+ export const parseAuthorizationHeader = (
12
+ header: string | string[] | undefined
13
+ ): { scheme: string, value: string } | null => {
14
+ const raw = Array.isArray(header) ? header[0] : header
15
+ if (raw == null) return null
16
+
17
+ const trimmed = raw.trim()
18
+ const space = trimmed.indexOf(' ')
19
+ if (space < 1) return null
20
+
21
+ const scheme = trimmed.slice(0, space).toLowerCase()
22
+ const value = trimmed.slice(space + 1).trim()
23
+ if (value.length < 1) return null
24
+
25
+ return { scheme, value }
26
+ }
27
+
28
+ /** Whether a value looks like an access token this deployment issued. */
29
+ export const isAccessToken = (value: string | null | undefined, prefix: string): boolean =>
30
+ value != null && value.length > prefix.length && value.startsWith(prefix)
31
+
32
+ /**
33
+ * The half of a token that may be shown again.
34
+ *
35
+ * Prefix plus the first characters of the secret. Enough to tell two tokens apart in a list, and
36
+ * far short of anything that could be replayed.
37
+ */
38
+ export const displayOf = (token: string, prefix: string): string => {
39
+ const secret = token.startsWith(prefix) ? token.slice(prefix.length) : token
40
+
41
+ return `${prefix}${secret.slice(0, AUTH_TOKEN_DISPLAY_LENGTH)}`
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './consts.js'
2
+ export * from './schemas.js'
3
+ export * from './entrypoints.js'
4
+ export * from './format.js'
5
+ export * from './carrier.js'
6
+ export type * from './types.js'
package/src/schemas.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { JSONSchemaType } from 'ajv'
2
+ import { AUTH_TOKEN_MAX_TTL, AUTH_TOKEN_NAME_MAX } from './consts.js'
3
+ import type { AccessTokenParams, CreateAccessToken } from './types.js'
4
+
5
+ export const CreateAccessTokenSchema: JSONSchemaType<CreateAccessToken> = {
6
+ type: 'object',
7
+ properties: {
8
+ name: { type: 'string', minLength: 1, maxLength: AUTH_TOKEN_NAME_MAX },
9
+ scopes: {
10
+ type: 'array',
11
+ nullable: true,
12
+ maxItems: 32,
13
+ items: { type: 'string', minLength: 1, maxLength: 128 },
14
+ },
15
+ expiresIn: {
16
+ type: 'integer',
17
+ nullable: true,
18
+ minimum: 60,
19
+ maximum: Math.floor(AUTH_TOKEN_MAX_TTL / 1000),
20
+ },
21
+ },
22
+ required: ['name'],
23
+ additionalProperties: false,
24
+ }
25
+
26
+ export const AccessTokenParamsSchema: JSONSchemaType<AccessTokenParams> = {
27
+ type: 'object',
28
+ properties: { id: { type: 'string', minLength: 1, maxLength: 128 } },
29
+ required: ['id'],
30
+ additionalProperties: false,
31
+ }
package/src/types.ts ADDED
@@ -0,0 +1,79 @@
1
+ import type { AuthRole } from '@owlmeans/auth'
2
+ import type { ResourceRecord } from '@owlmeans/resource'
3
+
4
+ /**
5
+ * One long-lived access token, as stored.
6
+ *
7
+ * The plaintext is never here. What is stored is its hash — a stolen database yields no usable
8
+ * credential — plus `display`, the prefix and a few characters, which is what a person sees in a
9
+ * list. A token is bound to the profile that minted it and can never outrank it: the guard
10
+ * intersects its scopes with the profile's on every request, so revoking a profile's access
11
+ * revokes every token it ever issued without touching a single token record.
12
+ */
13
+ export interface AccessTokenRecord extends ResourceRecord {
14
+ id?: string
15
+ /** SHA-256 of the plaintext, hex. The only copy of the secret that exists after issuance. */
16
+ hash: string
17
+ /** Prefix + the first characters of the secret. Shown in lists; useless as a credential. */
18
+ display: string
19
+ /** What the owner called it. */
20
+ name: string
21
+ userId: string
22
+ profileId: string
23
+ entityId: string
24
+ scopes: string[]
25
+ role: AuthRole
26
+ createdAt: Date
27
+ updatedAt?: Date
28
+ /** Written at most once per touch interval — a usage signal, not an access log. */
29
+ lastUsedAt?: Date
30
+ expiresAt?: Date
31
+ /** Set once and never unset. A revoked token is kept so its display name still resolves. */
32
+ revokedAt?: Date
33
+ }
34
+
35
+ /** What a caller may see. The hash never leaves the server. */
36
+ export type AccessTokenView = Omit<AccessTokenRecord, 'hash'>
37
+
38
+ export interface CreateAccessToken {
39
+ name: string
40
+ /** A subset of the caller's own scopes. Defaults to all of them. */
41
+ scopes?: string[]
42
+ /** Lifetime in seconds. Absent means no expiry; clamped to the maximum TTL. */
43
+ expiresIn?: number
44
+ }
45
+
46
+ /** The one moment the plaintext exists outside the caller's own machine. */
47
+ export interface IssuedAccessToken {
48
+ token: string
49
+ record: AccessTokenView
50
+ }
51
+
52
+ export interface AccessTokenList {
53
+ items: AccessTokenView[]
54
+ }
55
+
56
+ export interface AccessTokenParams {
57
+ id: string
58
+ }
59
+
60
+ /**
61
+ * How a client presents a token it already holds.
62
+ *
63
+ * `auth-token` is the OwlMeans scheme; `bearer` is what a third-party client — an MCP host reading
64
+ * a URL configuration, a curl script — will send whatever the documentation says. Both are
65
+ * accepted on the way in; a client chooses the one its transport is comfortable with.
66
+ */
67
+ export interface TokenCarrierOptions {
68
+ token: string | (() => string | Promise<string>)
69
+ scheme?: 'auth-token' | 'bearer'
70
+ }
71
+
72
+ export interface AuthTokenEntrypointOptions {
73
+ /** The entrypoint the token routes hang under. */
74
+ parent?: string
75
+ /** Path of the token base, relative to the parent. Defaults to `/tokens`. */
76
+ path?: string
77
+ /** The guard the base carries when it has no parent to inherit one from. */
78
+ guard?: string
79
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { AppType, makeBasicContext } from '@owlmeans/context'
3
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
4
+ import type { GuardService } from '@owlmeans/entrypoint'
5
+ import { makeTokenCarrierGuard } from '../src/carrier.js'
6
+
7
+ const contextWith = async (guard: GuardService): Promise<BasicContext<BasicConfig>> => {
8
+ const cfg: BasicConfig = { ready: false, service: 'carrier-tests', type: AppType.Backend, services: {} }
9
+ const context = makeBasicContext(cfg) as BasicContext<BasicConfig>
10
+ context.registerService(guard)
11
+ context.configure()
12
+ await context.init()
13
+
14
+ return context
15
+ }
16
+
17
+ describe('@owlmeans/auth-token — the client carrier guard', () => {
18
+ test('presents the OwlMeans scheme by default', async () => {
19
+ const guard = makeTokenCarrierGuard('carrier', { token: 'owl_secret' })
20
+ await contextWith(guard)
21
+
22
+ expect(await guard.authenticated()).toBe('AUTH-TOKEN owl_secret')
23
+ })
24
+
25
+ test('presents Bearer when asked — what a URL-configured client sends', async () => {
26
+ const guard = makeTokenCarrierGuard('carrier', { token: 'owl_secret', scheme: 'bearer' })
27
+ await contextWith(guard)
28
+
29
+ expect(await guard.authenticated()).toBe('Bearer owl_secret')
30
+ })
31
+
32
+ test('resolves a thunk on every call, so a reconfigured token is picked up', async () => {
33
+ let current = 'owl_first'
34
+ const guard = makeTokenCarrierGuard('carrier', { token: () => current })
35
+ await contextWith(guard)
36
+
37
+ expect(await guard.authenticated()).toBe('AUTH-TOKEN owl_first')
38
+ current = 'owl_second'
39
+ expect(await guard.authenticated()).toBe('AUTH-TOKEN owl_second')
40
+ })
41
+
42
+ test('holds no credential when the token is empty, and matches nothing', async () => {
43
+ const guard = makeTokenCarrierGuard('carrier', { token: '' })
44
+ await contextWith(guard)
45
+
46
+ expect(await guard.authenticated()).toBeNull()
47
+ expect(await guard.match({} as any, {} as any)).toBe(false)
48
+ })
49
+ })
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { displayOf, isAccessToken, parseAuthorizationHeader } from '../src/format.js'
3
+ import { AUTH_TOKEN_DEFAULT_PREFIX } from '../src/consts.js'
4
+
5
+ describe('@owlmeans/auth-token — Authorization parsing', () => {
6
+ test('lower-cases the scheme so Bearer and BEARER are one answer', () => {
7
+ expect(parseAuthorizationHeader('Bearer owl_abc')).toEqual({ scheme: 'bearer', value: 'owl_abc' })
8
+ expect(parseAuthorizationHeader('BEARER owl_abc')?.scheme).toBe('bearer')
9
+ expect(parseAuthorizationHeader('AUTH-TOKEN owl_abc')?.scheme).toBe('auth-token')
10
+ })
11
+
12
+ test('takes the first header when a proxy folded several', () => {
13
+ expect(parseAuthorizationHeader(['Bearer one', 'Bearer two'])?.value).toBe('one')
14
+ })
15
+
16
+ test('answers null for anything that is not a scheme and a value', () => {
17
+ expect(parseAuthorizationHeader(undefined)).toBeNull()
18
+ expect(parseAuthorizationHeader('')).toBeNull()
19
+ expect(parseAuthorizationHeader('Bearer')).toBeNull()
20
+ expect(parseAuthorizationHeader('Bearer ')).toBeNull()
21
+ expect(parseAuthorizationHeader(' leadingspace')).toBeNull()
22
+ })
23
+
24
+ test('keeps a value that itself contains spaces', () => {
25
+ expect(parseAuthorizationHeader('Bearer a b c')?.value).toBe('a b c')
26
+ })
27
+ })
28
+
29
+ describe('@owlmeans/auth-token — token shape', () => {
30
+ test('claims only a value carrying the prefix', () => {
31
+ expect(isAccessToken('owl_abcdef', AUTH_TOKEN_DEFAULT_PREFIX)).toBe(true)
32
+ expect(isAccessToken('vib_abcdef', AUTH_TOKEN_DEFAULT_PREFIX)).toBe(false)
33
+ expect(isAccessToken('abcdef', AUTH_TOKEN_DEFAULT_PREFIX)).toBe(false)
34
+ // The prefix alone is not a token — there is no secret behind it.
35
+ expect(isAccessToken(AUTH_TOKEN_DEFAULT_PREFIX, AUTH_TOKEN_DEFAULT_PREFIX)).toBe(false)
36
+ expect(isAccessToken(null, AUTH_TOKEN_DEFAULT_PREFIX)).toBe(false)
37
+ })
38
+
39
+ test('the display form keeps the prefix and eight characters of the secret', () => {
40
+ const display = displayOf('owl_ABCDEFGHIJKLMNOP', 'owl_')
41
+ expect(display).toBe('owl_ABCDEFGH')
42
+ // Short enough to be useless, long enough to tell two of a person's tokens apart.
43
+ expect(display.length).toBeLessThan('owl_ABCDEFGHIJKLMNOP'.length)
44
+ })
45
+ })
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.node.json"
5
+ ],
6
+ "compilerOptions": {
7
+ "types": ["bun"],
8
+ "rootDir": "../",
9
+ "noEmit": true
10
+ },
11
+ "include": ["./**/*", "../src/**/*"]
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": ["@owlmeans/dep-config/tsconfig.base.json"],
3
+ "compilerOptions": {
4
+ "rootDir": "./src/",
5
+ "outDir": "./build/"
6
+ },
7
+ "exclude": ["./dist/**/*", "./build/**/*", "./tests/**/*", "./*.ts"]
8
+ }