@owlmeans/auth-token 0.1.18-rc.32 → 0.1.18-rc.34
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 +164 -1
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/auth-token/SKILL.md +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,3 +1,166 @@
|
|
|
1
|
+
# @owlmeans/auth-token
|
|
2
|
+
|
|
3
|
+
The contract half of long-lived access tokens (API keys) on the OwlMeans auth rails: the stored
|
|
4
|
+
record shape, the token format and its deployment prefix, the three management route declarations
|
|
5
|
+
with their AJV schemas, `Authorization` header parsing that accepts `Bearer`, and a client-side
|
|
6
|
+
carrier guard that presents a token it was handed. A shared package imports it to declare the token
|
|
7
|
+
routes and types; a CLI, a test or an MCP connector imports it for the carrier guard. It stores and
|
|
8
|
+
verifies nothing — the token store, the verifying guard and the handlers are
|
|
9
|
+
`@owlmeans/server-auth-token`, and the management UI is `@owlmeans/web-auth-token`. Browser sessions
|
|
10
|
+
are `@owlmeans/client-auth`, not this package.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @owlmeans/auth-token@^0.1.18-rc.34
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Concepts
|
|
19
|
+
|
|
20
|
+
- **Token** — `<prefix><base58 of 24 random bytes>`. The prefix (`owl_` by default, overridden per
|
|
21
|
+
deployment) makes a token claimable: a guard matches only values that start with it, so an access
|
|
22
|
+
token and a session bearer share the `Authorization` header without shadowing each other.
|
|
23
|
+
- **Record** — `AccessTokenRecord`: the SHA-256 `hash` of the plaintext (never the plaintext), a
|
|
24
|
+
`display` form, the owning `userId`/`profileId`/`entityId`, `scopes`, `role`, timestamps and an
|
|
25
|
+
optional OAuth `audience`.
|
|
26
|
+
- **Display** — the prefix plus `AUTH_TOKEN_DISPLAY_LENGTH` (8) characters of the secret; enough to
|
|
27
|
+
tell tokens apart, useless as a credential. The plaintext appears once, in `IssuedAccessToken`.
|
|
28
|
+
- **Management surface** — `list`, `create` and `revoke` only. There is no update: scopes and
|
|
29
|
+
lifetime are fixed at issuance.
|
|
30
|
+
- **Audience** — `audience?: string[]` is set only on tokens minted through an OAuth grant; a guard
|
|
31
|
+
configured with `resources` admits such a token only when the lists intersect.
|
|
32
|
+
- **Carrier guard** — a client `GuardService` with no session, refresh or storage; `authMiddleware`
|
|
33
|
+
stamps its `authenticated()` answer onto the request header.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Declare the management routes
|
|
38
|
+
|
|
39
|
+
Mount the base under an account section so the ownership gate already guarding a person's settings
|
|
40
|
+
guards their tokens too.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { makeAuthTokenEntrypoints } from '@owlmeans/auth-token'
|
|
44
|
+
|
|
45
|
+
export const tokenEntrypoints = makeAuthTokenEntrypoints({ parent: account.base, path: '/tokens' })
|
|
46
|
+
|
|
47
|
+
context.registerEntrypoints(tokenEntrypoints)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A base without a parent carries `opts.guard` instead:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { GUARD_AUTH_TOKEN, makeAuthTokenEntrypoints } from '@owlmeans/auth-token'
|
|
54
|
+
|
|
55
|
+
const tokens = makeAuthTokenEntrypoints({ guard: GUARD_AUTH_TOKEN })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Authenticate a non-browser client with a token
|
|
59
|
+
|
|
60
|
+
Register the carrier under the alias the routes already name, and unchanged route declarations work
|
|
61
|
+
from a CLI or a connector.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { DEFAULT_GUARD, authMiddleware } from '@owlmeans/auth-common'
|
|
65
|
+
import { makeTokenCarrierGuard } from '@owlmeans/auth-token'
|
|
66
|
+
|
|
67
|
+
context.registerService(makeTokenCarrierGuard(DEFAULT_GUARD, {
|
|
68
|
+
token: () => process.env.OWL_TOKEN ?? '', // a thunk is re-read on every request
|
|
69
|
+
scheme: 'bearer', // default 'auth-token' → `AUTH-TOKEN <token>`
|
|
70
|
+
onRejected: async () => { await signInAgain() },
|
|
71
|
+
}))
|
|
72
|
+
context.registerMiddleware(authMiddleware)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Parse and display tokens
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { AUTH_TOKEN_DEFAULT_PREFIX, BEARER_SCHEME, AUTH_TOKEN_SCHEME, tokenFormatHelper } from '@owlmeans/auth-token'
|
|
79
|
+
|
|
80
|
+
const header = tokenFormatHelper.parseAuthorizationHeader(req.headers.authorization)
|
|
81
|
+
if (header != null && (header.scheme === BEARER_SCHEME || header.scheme === AUTH_TOKEN_SCHEME)
|
|
82
|
+
&& tokenFormatHelper.isAccessToken(header.value, AUTH_TOKEN_DEFAULT_PREFIX)) {
|
|
83
|
+
// claim it
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
tokenFormatHelper.displayOf('owl_ABCDEFGHIJKLMNOP', 'owl_') // 'owl_ABCDEFGH'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Create a token from a client
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import type { CreateAccessToken, IssuedAccessToken } from '@owlmeans/auth-token'
|
|
93
|
+
|
|
94
|
+
const body: CreateAccessToken = { name: 'CI deploy', scopes: ['project:read'], expiresIn: 30 * 24 * 3600 }
|
|
95
|
+
const issued: IssuedAccessToken = await context.entrypoint(tokenEntrypoints.create).call({ body })
|
|
96
|
+
// issued.token is the only time the plaintext is available
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### Functions and helpers
|
|
102
|
+
|
|
103
|
+
| Symbol | Kind | Purpose |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `makeAuthTokenEntrypoints(opts?)` | function | `{ base, list, create, revoke }` protocols: `GET /`, `POST /`, `DELETE /:id` under `opts.path ?? '/tokens'`; `opts.parent` or `opts.guard` for the base |
|
|
106
|
+
| `makeTokenCarrierGuard(alias, opts)` | function | client `GuardService`; `authenticated()` → `AUTH-TOKEN <t>` or `Bearer <t>`, `null` for an empty token; `update()` calls `opts.onRejected` |
|
|
107
|
+
| `tokenFormatHelper`, `createTokenFormatHelper()` | helper | the three members below |
|
|
108
|
+
| `tokenFormatHelper.parseAuthorizationHeader(header)` | member | `{ scheme, value }` with scheme lower-cased; first of a folded array; `null` without a value |
|
|
109
|
+
| `tokenFormatHelper.isAccessToken(value, prefix)` | member | value starts with the prefix and is longer than it |
|
|
110
|
+
| `tokenFormatHelper.displayOf(token, prefix)` | member | prefix + 8 characters of the secret |
|
|
111
|
+
| `CreateAccessTokenSchema` | AJV schema | `name` 1–64, `scopes?` ≤ 32 items, `expiresIn?` integer seconds 60…366 days |
|
|
112
|
+
| `AccessTokenParamsSchema` | AJV schema | `{ id }`, 1–128 characters |
|
|
113
|
+
|
|
114
|
+
### Constants
|
|
115
|
+
|
|
116
|
+
| Symbol | Value |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `GUARD_AUTH_TOKEN` | `'guard:auth-token'` |
|
|
119
|
+
| `AUTH_TOKEN_RESOURCE`, `AUTH_TOKEN_COLLECTION` | `'auth-token:token'`, `'access-token'` |
|
|
120
|
+
| `AUTH_TOKEN_DEFAULT_PREFIX` | `'owl_'` |
|
|
121
|
+
| `AUTH_TOKEN_SECRET_BYTES`, `AUTH_TOKEN_DISPLAY_LENGTH` | `24`, `8` |
|
|
122
|
+
| `AUTH_TOKEN_TOUCH_INTERVAL` | 5 minutes in ms — how often `lastUsedAt` is written |
|
|
123
|
+
| `AUTH_TOKEN_MAX_TTL` | 366 days in ms |
|
|
124
|
+
| `AUTH_TOKEN_NAME_MAX` | `64` |
|
|
125
|
+
| `authToken` | frozen route aliases `{ base, list, create, revoke }` (`'auth-token:*'`) |
|
|
126
|
+
| `AUTH_TOKEN_SCHEME`, `BEARER_SCHEME` | `'auth-token'`, `'bearer'` (lower-cased, for comparison) |
|
|
127
|
+
|
|
128
|
+
### Types
|
|
129
|
+
|
|
130
|
+
| Symbol | Purpose |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `AccessTokenRecord` | the stored record (hash, display, owner ids, scopes, role, dates, `audience?`) |
|
|
133
|
+
| `AccessTokenView` | the record without `hash` — what a caller may see |
|
|
134
|
+
| `CreateAccessToken` | `{ name, scopes?, expiresIn? }` — `expiresIn` in seconds |
|
|
135
|
+
| `IssuedAccessToken` | `{ token, record }` |
|
|
136
|
+
| `AccessTokenList`, `AccessTokenParams` | `{ items }`, `{ id }` |
|
|
137
|
+
| `TokenCarrierOptions` | `{ token: string \| () => string \| Promise<string>, scheme?, onRejected? }` |
|
|
138
|
+
| `AuthTokenEntrypointOptions`, `AuthTokenEntrypoints` | `makeAuthTokenEntrypoints` input and output |
|
|
139
|
+
| `AuthorizationHeader`, `TokenFormatHelper` | parsed header and the format helper interface |
|
|
140
|
+
|
|
141
|
+
## Common pitfalls
|
|
142
|
+
|
|
143
|
+
- Do not use `extractAuthToken` from `@owlmeans/auth-common` for these tokens — it compares against
|
|
144
|
+
the upper-cased type and never matches `Bearer`; use `tokenFormatHelper.parseAuthorizationHeader`.
|
|
145
|
+
- `expiresIn` is seconds, not days; omit it for "never" rather than sending `0`.
|
|
146
|
+
- The plaintext is returned only by `create`; never store or log it. Lists show `display`.
|
|
147
|
+
- Give each deployment its own prefix so two deployments never claim each other's tokens.
|
|
148
|
+
- Pass a thunk as `token` in long-running processes so a reconfigured credential is picked up.
|
|
149
|
+
- Do not add an update route: a token that can be widened after issuance is a grant nobody can
|
|
150
|
+
reason about.
|
|
151
|
+
|
|
152
|
+
The `auth-token` skill covers the prefix rule, header parsing, the carrier guard and the management
|
|
153
|
+
surface; the `server-auth-token` skill covers the store and the verifying guard.
|
|
154
|
+
|
|
155
|
+
## Related packages
|
|
156
|
+
|
|
157
|
+
- [`@owlmeans/server-auth-token`](../server-auth-token) — token store, verifying guard with audience admission, handlers
|
|
158
|
+
- [`@owlmeans/web-auth-token`](../web-auth-token) — the token-management panel and hook
|
|
159
|
+
- [`@owlmeans/server-oauth`](../server-oauth) — mints audience-scoped tokens through OAuth grants
|
|
160
|
+
- [`@owlmeans/auth-common`](../auth-common) — `authMiddleware`, `DEFAULT_GUARD`
|
|
161
|
+
- [`@owlmeans/cli-auth`](../cli-auth) — browser sign-in for a CLI, whose result a carrier guard can present
|
|
162
|
+
- [`@owlmeans/entrypoint`](../entrypoint) — protocol declarations and `GuardService`
|
|
163
|
+
|
|
1
164
|
<!-- owlmeans:agent-guidance:start -->
|
|
2
165
|
## Agent guidance
|
|
3
166
|
|
|
@@ -6,7 +169,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
6
169
|
your project's skill store (`.agents/skills/`):
|
|
7
170
|
|
|
8
171
|
```sh
|
|
9
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
172
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.57
|
|
10
173
|
```
|
|
11
174
|
|
|
12
175
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/auth-token",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.34",
|
|
5
|
+
"generatedAt": "2026-10-20T23:19:58.633Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/auth-token
|
|
9
9
|
|
|
10
10
|
**Layer:** Auth shared
|
|
11
|
-
**Install:** `"@owlmeans/auth-token": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/auth-token": "^0.1.18-rc.34"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
The contract half of long-lived access tokens: the record shape, the route declarations, the
|
|
14
14
|
format helpers, and one client-side guard that presents a token it was handed. The server half —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/auth-token",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.34",
|
|
4
4
|
"license": "MIT",
|
|
5
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
6
|
"type": "module",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
27
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
28
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
29
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
30
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
26
|
+
"@owlmeans/auth": "^0.1.18-rc.42",
|
|
27
|
+
"@owlmeans/context": "^0.1.18-rc.35",
|
|
28
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.42",
|
|
29
|
+
"@owlmeans/resource": "^0.1.18-rc.41",
|
|
30
|
+
"@owlmeans/route": "^0.1.18-rc.36"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@owlmeans/dep-config": "workspace:*",
|