@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.
- package/README.md +15 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/server-auth-token/SKILL.md +136 -0
- package/build/append.d.ts +20 -0
- package/build/append.d.ts.map +1 -0
- package/build/append.js +29 -0
- package/build/append.js.map +1 -0
- package/build/coguard.d.ts +16 -0
- package/build/coguard.d.ts.map +1 -0
- package/build/coguard.js +23 -0
- package/build/coguard.js.map +1 -0
- package/build/consts.d.ts +3 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +3 -0
- package/build/consts.js.map +1 -0
- package/build/guard.d.ts +24 -0
- package/build/guard.d.ts.map +1 -0
- package/build/guard.js +126 -0
- package/build/guard.js.map +1 -0
- package/build/handlers/index.d.ts +4 -0
- package/build/handlers/index.d.ts.map +1 -0
- package/build/handlers/index.js +94 -0
- package/build/handlers/index.js.map +1 -0
- package/build/hash.d.ts +17 -0
- package/build/hash.d.ts.map +1 -0
- package/build/hash.js +19 -0
- package/build/hash.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +8 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +14 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +23 -0
- package/build/resource.js.map +1 -0
- package/build/types.d.ts +36 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +48 -0
- package/src/append.ts +34 -0
- package/src/coguard.ts +23 -0
- package/src/consts.ts +2 -0
- package/src/guard.ts +143 -0
- package/src/handlers/index.ts +115 -0
- package/src/hash.ts +27 -0
- package/src/index.ts +8 -0
- package/src/resource.ts +28 -0
- package/src/types.ts +39 -0
- package/tests/coguard.spec.ts +36 -0
- package/tests/context.ts +109 -0
- package/tests/guard.spec.ts +128 -0
- package/tests/handlers.spec.ts +154 -0
- package/tests/tsconfig.json +12 -0
- 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"}
|
package/build/append.js
ADDED
|
@@ -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"}
|
package/build/coguard.js
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,sBAAsB,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -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"}
|
package/build/guard.d.ts
ADDED
|
@@ -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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"}
|
package/build/hash.d.ts
ADDED
|
@@ -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"}
|
package/build/index.d.ts
ADDED
|
@@ -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
|