@vantageos/cloud-identity 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +72 -0
- package/dist/crypto.d.ts +12 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +18 -0
- package/dist/crypto.js.map +1 -1
- package/dist/human-path.d.ts +46 -0
- package/dist/human-path.d.ts.map +1 -1
- package/dist/human-path.js +63 -2
- package/dist/human-path.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/org-guard.d.ts +48 -0
- package/dist/org-guard.d.ts.map +1 -1
- package/dist/org-guard.js +48 -0
- package/dist/org-guard.js.map +1 -1
- package/dist/presented-bearer.d.ts +205 -0
- package/dist/presented-bearer.d.ts.map +1 -0
- package/dist/presented-bearer.js +226 -0
- package/dist/presented-bearer.js.map +1 -0
- package/dist/tenancy-domain.d.ts +6 -3
- package/dist/tenancy-domain.d.ts.map +1 -1
- package/dist/tenancy-domain.js +6 -3
- package/dist/tenancy-domain.js.map +1 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,121 @@
|
|
|
3
3
|
All notable changes to `@vantageos/cloud-identity` are documented here.
|
|
4
4
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [0.7.0]
|
|
7
|
+
|
|
8
|
+
**Additive — resolution of a PRESENTED, non-master bearer.** The published
|
|
9
|
+
0.5.0 surface covered the master credential (`validateMasterBearer`) and a
|
|
10
|
+
decoder that verifies nothing and says so (`decodeUnverifiedBearer`). Between
|
|
11
|
+
them there was no function taking a presented non-master bearer, hashing it,
|
|
12
|
+
looking the row up and returning a resolved identity or a typed refusal — so a
|
|
13
|
+
consuming product wrote that primitive locally, against an untyped client, with
|
|
14
|
+
the lookup failure swallowed into `null`. That is a second authority answering
|
|
15
|
+
"who is calling". This release is the single one, so the local variant can be
|
|
16
|
+
deleted rather than duplicated.
|
|
17
|
+
|
|
18
|
+
**Version number.** This entry takes `0.7.0`, not `0.6.0`. An unmerged branch
|
|
19
|
+
already claims `0.6.0` for `resolveTenantIdOrAbsent` (the non-throwing sibling
|
|
20
|
+
of `requireTenantId`); leaving `0.6.0` to it means these two can land in either
|
|
21
|
+
order without either having to be renumbered after review.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `validatePresentedBearer(authHeader, deps)` (`src/presented-bearer.ts`) —
|
|
26
|
+
parses the header, hashes the token to a SHA-256 hex digest, obtains the row
|
|
27
|
+
through the caller-supplied `deps.lookupBySecretHash(digest)`, re-compares the
|
|
28
|
+
row's stored digest with `timingSafeEqual`, then honours row shape, revocation
|
|
29
|
+
and expiry. The package cannot query a datastore, so the read is injected; every
|
|
30
|
+
decision stays in the package. `deps.now` is an injectable clock.
|
|
31
|
+
- **The callback receives the DIGEST, never the token**, keeping a usable
|
|
32
|
+
secret out of a consumer's query and out of its query logs.
|
|
33
|
+
- **An unknown digest, a revoked row, an expired row, a row of invalid shape
|
|
34
|
+
and a row whose stored digest does not match all return ONE shared frozen
|
|
35
|
+
value**, `{ ok: false, error: "mismatch" }` — one object rather than five
|
|
36
|
+
equal literals, so the branches cannot drift apart by an added field. A
|
|
37
|
+
prober cannot learn whether a token was ever real. `deps.onRefusal` reports
|
|
38
|
+
the internal distinction (`PresentedBearerInternalReason`) to the consumer's
|
|
39
|
+
own logs; a throwing sink cannot change the outcome.
|
|
40
|
+
- **A lookup that throws is a REFUSAL** (`error: "unavailable"`), deliberately
|
|
41
|
+
outside the collapsed bucket so a datastore outage can be answered 503
|
|
42
|
+
rather than reported to every caller as a bad credential. No branch grants
|
|
43
|
+
anything on a lookup failure.
|
|
44
|
+
- Header vocabulary is `validateMasterBearer`'s, unchanged: `"missing"`,
|
|
45
|
+
`"malformed"`, `"mismatch"`. A malformed header costs no datastore read.
|
|
46
|
+
- `requireAgentScopedIdentity(identity)` — the second, mandatory call for a
|
|
47
|
+
per-agent surface. An organization-wide token (a row with no agent identity)
|
|
48
|
+
resolves successfully and carries its agent as an EXPLICIT absence
|
|
49
|
+
(`AgentPresence`, a union that must be narrowed — not a nullable field a
|
|
50
|
+
caller can fail to check). The per-agent form `AgentScopedIdentity` carries a
|
|
51
|
+
module-private type brand, so it cannot be constructed outside the package: a
|
|
52
|
+
handler typed against it is reachable only through this function. Its refusal
|
|
53
|
+
is distinguishable from a bad credential on purpose — 403, not 401.
|
|
54
|
+
- `sha256Hex(input)` (`src/crypto.ts`) — lower-case hex SHA-256 of a string,
|
|
55
|
+
exported so the code WRITING a token row derives the digest exactly as the
|
|
56
|
+
resolver recomputes it.
|
|
57
|
+
- `storedBearerRowSchema` + `StoredBearerRow`, `AgentPresence`,
|
|
58
|
+
`ResolvedBearerIdentity`, `AgentScopedIdentity`,
|
|
59
|
+
`ValidatePresentedBearerResult`, `RequireAgentScopedResult`,
|
|
60
|
+
`PresentedBearerDeps`, `PresentedBearerInternalReason`.
|
|
61
|
+
- Subpath export `@vantageos/cloud-identity/presented-bearer`.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- Documentation only: `decodeUnverifiedBearer`'s security banner said the
|
|
66
|
+
production trust boundary was "not yet exported by this package". For an
|
|
67
|
+
opaque token it now is, and the banner names it. No behaviour change.
|
|
68
|
+
|
|
69
|
+
### Unchanged
|
|
70
|
+
|
|
71
|
+
- **Every existing export keeps its exact behaviour and signature.** The new
|
|
72
|
+
module is a new file; the only edits to existing sources are one appended
|
|
73
|
+
function in `src/crypto.ts`, new export lines in `src/index.ts`, and the
|
|
74
|
+
docstring above. No existing test was edited.
|
|
75
|
+
|
|
76
|
+
## [0.6.0]
|
|
77
|
+
|
|
78
|
+
**Additive — typed refusal for reads, and a role assertion.**
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- `resolveTenantIdOrAbsent(source)` (`src/org-guard.ts`): non-throwing sibling
|
|
83
|
+
of `requireTenantId`. Returns `{ present: true, tenantId }` or a typed
|
|
84
|
+
`{ present: false, absence: { code: "TENANT_ABSENT", reason } }`. A read that
|
|
85
|
+
throws crashes a mounted render; a read that returns a bare empty value is
|
|
86
|
+
byte-identical to "there is nothing". This returns neither.
|
|
87
|
+
- `requireHumanRole(session, required)` and
|
|
88
|
+
`resolveHumanRoleOrRefusal(session, required)` (`src/human-path.ts`): role
|
|
89
|
+
assertion on the verified session's `orgRole`, throwing and non-throwing
|
|
90
|
+
forms. Exact match, no hierarchy; an empty required list admits nobody.
|
|
91
|
+
- Types: `TenantAbsence`, `TenantAbsenceReason`, `TenantResolution`,
|
|
92
|
+
`RequiredHumanRole`, `RoleRefusal`, `RoleRefusalReason`, `RoleResolution`.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- The Clerk org-role map is now prototype-less (`Object.create(null)`). As a
|
|
97
|
+
plain object literal it inherited `Object.prototype`, so a presented
|
|
98
|
+
`orgRole` of `toString`, `constructor`, `valueOf` or `__proto__` did not
|
|
99
|
+
MISS the lookup — it returned an inherited value, which is truthy, and the
|
|
100
|
+
resolver handed back a FUNCTION as the role instead of refusing. Any key
|
|
101
|
+
that is not one of the eight own keys is now `undefined`, so the existing
|
|
102
|
+
refusal path rejects it. Fixed at the data; no lookup expression and no
|
|
103
|
+
function body changed, and no list of forbidden names was introduced.
|
|
104
|
+
|
|
105
|
+
### Unchanged
|
|
106
|
+
|
|
107
|
+
- `requireTenantId` and `normalizeVerifiedHumanSession` are byte-for-byte
|
|
108
|
+
unchanged and still throw on every refusal.
|
|
109
|
+
- The tenant key stays opaque: no export takes or returns a slug. That
|
|
110
|
+
opacity is now pinned on all four served paths (session, bearer,
|
|
111
|
+
self-host, `role.identity.tenant`), through BOTH the throwing and the
|
|
112
|
+
non-throwing sibling, with a fixture carrying edge whitespace so that
|
|
113
|
+
splicing a `.trim()` onto a served id goes RED instead of passing.
|
|
114
|
+
|
|
115
|
+
### Removed
|
|
116
|
+
|
|
117
|
+
- `bun.lock`, committed by accident in 0.5.0. `package-lock.json` is the
|
|
118
|
+
lockfile of record; two lockfiles for two package managers guarantee a
|
|
119
|
+
silent divergence.
|
|
120
|
+
|
|
6
121
|
## [0.5.0]
|
|
7
122
|
|
|
8
123
|
**Additive — grant-aware scope filter.** Closes the other pole of the
|
package/README.md
CHANGED
|
@@ -105,9 +105,72 @@ await resolveTenant({ session: null }); // throws Error("Unauthenticated: no ses
|
|
|
105
105
|
`{ kind: "bearer", context }` for a machine caller. A missing session, a
|
|
106
106
|
missing organization and an empty organization id are all refusals, never a
|
|
107
107
|
default.
|
|
108
|
+
- `resolveTenantIdOrAbsent(source)` — the NON-throwing sibling of
|
|
109
|
+
`requireTenantId`, for a public READ. Same `TenantSource` union, same
|
|
110
|
+
refusal conditions; instead of throwing it returns
|
|
111
|
+
`{ present: true, tenantId }` or
|
|
112
|
+
`{ present: false, absence: { code: "TENANT_ABSENT", reason } }`
|
|
113
|
+
(`TenantAbsence`, `TenantAbsenceReason`, `TenantResolution`). The absence
|
|
114
|
+
is a non-empty object: it is never `[]`, `{}`, `null` or a zeroed count, so
|
|
115
|
+
"no organization" and "an organization with nothing in it" cannot collapse
|
|
116
|
+
into one value. Use `requireTenantId` at a WRITE boundary and
|
|
117
|
+
`resolveTenantIdOrAbsent` where a throw would crash a mounted render. The
|
|
118
|
+
tenant id is OPAQUE — returned byte-for-byte, never a slug or display name.
|
|
119
|
+
- `requireHumanRole(session, required)` — asserts the VERIFIED session
|
|
120
|
+
carries `required` (a `HumanAccountRole` or a list of them, exact match, no
|
|
121
|
+
hierarchy) and returns the resolved identity; throws otherwise. The role is
|
|
122
|
+
read from `session.orgRole`, never from an argument. `RequiredHumanRole`
|
|
123
|
+
is its `required` type.
|
|
124
|
+
- `resolveHumanRoleOrRefusal(session, required)` — the non-throwing form:
|
|
125
|
+
`{ admitted: true, identity }` or
|
|
126
|
+
`{ admitted: false, refusal: { code: "ROLE_REFUSED", reason } }`
|
|
127
|
+
(`RoleResolution`, `RoleRefusal`, `RoleRefusalReason`). `reason` is
|
|
128
|
+
`role-not-held` for a verified member lacking the role.
|
|
108
129
|
- `getEffectiveTenantId(ctx, args)` — resolves which tenant a request should
|
|
109
130
|
act on when a caller may legitimately act on more than one.
|
|
110
131
|
|
|
132
|
+
**Resolving a presented bearer token**
|
|
133
|
+
|
|
134
|
+
- `validatePresentedBearer(authHeader, deps)` — takes the raw
|
|
135
|
+
`Authorization` header a non-master caller presented, and returns either a
|
|
136
|
+
resolved identity or a coarse refusal. It hashes the token, asks your
|
|
137
|
+
`deps.lookupBySecretHash` callback for the row carrying that digest,
|
|
138
|
+
re-compares the stored digest in constant time, and then checks revocation
|
|
139
|
+
and expiry. Your callback does one indexed read; every decision stays in the
|
|
140
|
+
package, so two consumers cannot drift into two different answers to "who is
|
|
141
|
+
calling".
|
|
142
|
+
- **Your callback receives the digest, never the token.** A raw secret
|
|
143
|
+
therefore never reaches your query, your query log or your slow-query log.
|
|
144
|
+
Use `sha256Hex` to produce the same digest when you WRITE a token row, so
|
|
145
|
+
the two sides agree.
|
|
146
|
+
- **An unknown token, a revoked token and an expired token return one
|
|
147
|
+
identical value** (`{ ok: false, error: "mismatch" }`). Someone probing
|
|
148
|
+
your endpoint cannot learn from the reply whether a token was ever real.
|
|
149
|
+
Pass `deps.onRefusal` to receive the distinction
|
|
150
|
+
(`PresentedBearerInternalReason`) in your own logs, where it belongs.
|
|
151
|
+
- **A failing lookup refuses.** If your callback throws, the result is
|
|
152
|
+
`{ ok: false, error: "unavailable" }` — a refusal you can answer with 503
|
|
153
|
+
instead of 401. There is no branch that grants anything when the lookup
|
|
154
|
+
fails.
|
|
155
|
+
- `deps.now` is an injectable clock, so expiry is testable without waiting.
|
|
156
|
+
- Types: `PresentedBearerDeps`, `StoredBearerRow` (validated at runtime by
|
|
157
|
+
`storedBearerRowSchema`), `ResolvedBearerIdentity`,
|
|
158
|
+
`ValidatePresentedBearerResult`.
|
|
159
|
+
- `requireAgentScopedIdentity(identity)` — narrows a resolved identity to a
|
|
160
|
+
surface that acts as ONE agent, and refuses when the token has no agent.
|
|
161
|
+
A token can be organization-wide: it names a tenant and no agent. That is a
|
|
162
|
+
legitimate credential, and it must never be silently widened into whichever
|
|
163
|
+
agent the surface happened to be about. So the agent is not a nullable field
|
|
164
|
+
you have to remember to check — `ResolvedBearerIdentity.agent` is an
|
|
165
|
+
`AgentPresence`, a union you must narrow, and the per-agent form
|
|
166
|
+
(`AgentScopedIdentity`, with a plain `agentId: string`) can only be obtained
|
|
167
|
+
from this function. Returns `RequireAgentScopedResult`; its refusal is
|
|
168
|
+
distinct from a bad credential, because the right answer there is 403, not
|
|
169
|
+
401.
|
|
170
|
+
- `sha256Hex(input)` — the lower-case hex SHA-256 digest of a string. Exported
|
|
171
|
+
so that the code storing a token row hashes it exactly the way the resolver
|
|
172
|
+
will recompute it. Hash once, store the digest, discard the token.
|
|
173
|
+
|
|
111
174
|
**Row filtering**
|
|
112
175
|
|
|
113
176
|
- `passesScopeFilter(ctx, row)` — true when this row is visible to this
|
|
@@ -228,6 +291,15 @@ Worth reading before you rely on it.
|
|
|
228
291
|
- **It does not talk to your database.** `scopeFilterList` filters rows you
|
|
229
292
|
have already fetched. If fetching them was itself expensive or unsafe, filter
|
|
230
293
|
earlier, in your query.
|
|
294
|
+
- **It does not issue, rotate or revoke tokens.** `validatePresentedBearer`
|
|
295
|
+
reads a row you already store and decides whether it may be believed. Minting
|
|
296
|
+
a token, writing its row, marking it revoked and expiring it are yours; the
|
|
297
|
+
package only insists that a row it is handed be believed in constant time,
|
|
298
|
+
and refused identically however it fails.
|
|
299
|
+
- **It does not verify a signed JWT.** The presented-bearer path is for an
|
|
300
|
+
OPAQUE token — one whose only meaning is the row it matches. If your
|
|
301
|
+
credential carries its own signed claims, verify that signature with a JWT
|
|
302
|
+
library first; this package exports no verifier.
|
|
231
303
|
|
|
232
304
|
## Upgrading to 0.4.0
|
|
233
305
|
|
package/dist/crypto.d.ts
CHANGED
|
@@ -15,4 +15,16 @@
|
|
|
15
15
|
* Deno, edge runtimes) and parity with the original Convex implementation.
|
|
16
16
|
*/
|
|
17
17
|
export declare function timingSafeEqual(a: Uint8Array, b: Uint8Array): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* SHA-256 of a UTF-8 string as a lower-case hex digest.
|
|
20
|
+
*
|
|
21
|
+
* Exported because a consumer that STORES a bearer row must produce the digest
|
|
22
|
+
* the same way the resolver will recompute it. Without this, each consumer
|
|
23
|
+
* hashes its own way and the mismatch surfaces as "correct token refused".
|
|
24
|
+
*
|
|
25
|
+
* @security Hash the token once, store the digest, discard the token. This
|
|
26
|
+
* function is one-way; it is not a substitute for a comparison — compare
|
|
27
|
+
* digests with `timingSafeEqual`, never with `===`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sha256Hex(input: string): Promise<string>;
|
|
18
30
|
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACnC,CAAC,EAAE,UAAU,EACb,CAAC,EAAE,UAAU,GACZ,OAAO,CAAC,OAAO,CAAC,CAoBlB"}
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACnC,CAAC,EAAE,UAAU,EACb,CAAC,EAAE,UAAU,GACZ,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9D"}
|
package/dist/crypto.js
CHANGED
|
@@ -29,4 +29,22 @@ export async function timingSafeEqual(a, b) {
|
|
|
29
29
|
}
|
|
30
30
|
return diff === 0;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* SHA-256 of a UTF-8 string as a lower-case hex digest.
|
|
34
|
+
*
|
|
35
|
+
* Exported because a consumer that STORES a bearer row must produce the digest
|
|
36
|
+
* the same way the resolver will recompute it. Without this, each consumer
|
|
37
|
+
* hashes its own way and the mismatch surfaces as "correct token refused".
|
|
38
|
+
*
|
|
39
|
+
* @security Hash the token once, store the digest, discard the token. This
|
|
40
|
+
* function is one-way; it is not a substitute for a comparison — compare
|
|
41
|
+
* digests with `timingSafeEqual`, never with `===`.
|
|
42
|
+
*/
|
|
43
|
+
export async function sha256Hex(input) {
|
|
44
|
+
const data = new TextEncoder().encode(input);
|
|
45
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
46
|
+
return Array.from(new Uint8Array(digest))
|
|
47
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
48
|
+
.join("");
|
|
49
|
+
}
|
|
32
50
|
//# sourceMappingURL=crypto.js.map
|
package/dist/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,CAAa,EACb,CAAa;IAEb,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,6EAA6E;QAC7E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACxC,KAAK,EACL,WAAsC,EACtC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;QACF,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAgC,CAAC,CAAC;QACzE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IACxB,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC"}
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,CAAa,EACb,CAAa;IAEb,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,6EAA6E;QAC7E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACxC,KAAK,EACL,WAAsC,EACtC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;QACF,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAgC,CAAC,CAAC;QACzE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IACxB,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAa;IAC3C,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACjD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}
|
package/dist/human-path.d.ts
CHANGED
|
@@ -92,4 +92,50 @@ export type ResolvedHumanIdentity = {
|
|
|
92
92
|
* ```
|
|
93
93
|
*/
|
|
94
94
|
export declare function normalizeVerifiedHumanSession(session: ClerkSessionLike | null | undefined): ResolvedHumanIdentity;
|
|
95
|
+
/**
|
|
96
|
+
* Why a caller was not admitted. `role-not-held` is the one that matters: a
|
|
97
|
+
* verified member of the organization who does not carry the required role.
|
|
98
|
+
*/
|
|
99
|
+
export type RoleRefusalReason = "no-session" | "no-organization" | "no-verified-role" | "role-not-held";
|
|
100
|
+
/** The typed refusal. A non-empty object carrying its own code. */
|
|
101
|
+
export type RoleRefusal = {
|
|
102
|
+
code: "ROLE_REFUSED";
|
|
103
|
+
reason: RoleRefusalReason;
|
|
104
|
+
};
|
|
105
|
+
/** Result of `resolveHumanRoleOrRefusal`. Branch on `admitted`. */
|
|
106
|
+
export type RoleResolution = {
|
|
107
|
+
admitted: true;
|
|
108
|
+
identity: ResolvedHumanIdentity;
|
|
109
|
+
} | {
|
|
110
|
+
admitted: false;
|
|
111
|
+
refusal: RoleRefusal;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* `required` is the POLICY the call site declares; the caller's role is the
|
|
115
|
+
* CLAIM, and it is read only from `session.orgRole` (which the caller has
|
|
116
|
+
* already verified upstream — see the @security note on
|
|
117
|
+
* `normalizeVerifiedHumanSession`). There is no parameter through which a
|
|
118
|
+
* caller can supply its own role. Matching is EXACT, with no hierarchy:
|
|
119
|
+
* `owner` does not satisfy `"admin"`; pass `["owner", "admin"]` to admit
|
|
120
|
+
* either. An empty list admits nobody.
|
|
121
|
+
*/
|
|
122
|
+
export type RequiredHumanRole = HumanAccountRole | readonly HumanAccountRole[];
|
|
123
|
+
/**
|
|
124
|
+
* THROWING form, for a write boundary. Returns the resolved identity when
|
|
125
|
+
* the verified session carries one of the `required` roles; throws
|
|
126
|
+
* otherwise. Malformed sessions throw exactly what
|
|
127
|
+
* `normalizeVerifiedHumanSession` throws; a well-formed session lacking the
|
|
128
|
+
* role throws "Role refused: ...".
|
|
129
|
+
*
|
|
130
|
+
* Same `@security` precondition as `normalizeVerifiedHumanSession`: this
|
|
131
|
+
* asserts on a session that is ALREADY verified. It verifies nothing.
|
|
132
|
+
*/
|
|
133
|
+
export declare function requireHumanRole(session: ClerkSessionLike | null | undefined, required: RequiredHumanRole): ResolvedHumanIdentity;
|
|
134
|
+
/**
|
|
135
|
+
* NON-THROWING form, for a public read. Returns `{ admitted: true, identity }`
|
|
136
|
+
* or `{ admitted: false, refusal }` where the refusal carries `ROLE_REFUSED`
|
|
137
|
+
* and a reason. Admits exactly the sessions `requireHumanRole` returns for
|
|
138
|
+
* and refuses exactly the ones it throws for.
|
|
139
|
+
*/
|
|
140
|
+
export declare function resolveHumanRoleOrRefusal(session: ClerkSessionLike | null | undefined, required: RequiredHumanRole): RoleResolution;
|
|
95
141
|
//# sourceMappingURL=human-path.d.ts.map
|
package/dist/human-path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"human-path.d.ts","sourceRoot":"","sources":["../src/human-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,sBAAsB;;;;;EAKjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAMtE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAMF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;CACxB,CAAC;
|
|
1
|
+
{"version":3,"file":"human-path.d.ts","sourceRoot":"","sources":["../src/human-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,sBAAsB;;;;;EAKjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAMtE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAMF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;CACxB,CAAC;AAgCF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAC3C,qBAAqB,CAiCvB;AAMD;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ,iBAAiB,GACjB,kBAAkB,GAClB,eAAe,CAAC;AAEpB,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,iBAAiB,CAAC;CAC3B,CAAC;AAEF,mEAAmE;AACnE,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,qBAAqB,CAAA;CAAE,GACnD;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,SAAS,gBAAgB,EAAE,CAAC;AAM/E;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC5C,QAAQ,EAAE,iBAAiB,GAC1B,qBAAqB,CAWvB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC5C,QAAQ,EAAE,iBAAiB,GAC1B,cAAc,CAqBhB"}
|
package/dist/human-path.js
CHANGED
|
@@ -35,7 +35,17 @@ export const humanAccountRoleSchema = z.enum([
|
|
|
35
35
|
* listed here is refused rather than silently coerced to a default, per
|
|
36
36
|
* "presented, never inferred from an absence."
|
|
37
37
|
*/
|
|
38
|
-
const CLERK_ORG_ROLE_MAP =
|
|
38
|
+
const CLERK_ORG_ROLE_MAP = Object.assign(
|
|
39
|
+
// PROTOTYPE-LESS on purpose. As a plain object literal this map INHERITED
|
|
40
|
+
// `Object.prototype`, so a lookup of a presented role like `toString`,
|
|
41
|
+
// `constructor` or `__proto__` did not MISS — it returned an inherited
|
|
42
|
+
// value, which is truthy, and the resolver handed back a FUNCTION as the
|
|
43
|
+
// role instead of refusing. With a null prototype, any key that is not one
|
|
44
|
+
// of the eight own keys below is `undefined`, so the existing
|
|
45
|
+
// `if (!mapped) throw` refuses it. The lookup expressions in
|
|
46
|
+
// `normalizeVerifiedHumanSession` and `resolveHumanRoleOrRefusal` are
|
|
47
|
+
// unchanged — this is a fix at the DATA, not a list of forbidden names.
|
|
48
|
+
Object.create(null), {
|
|
39
49
|
"org:owner": "owner",
|
|
40
50
|
owner: "owner",
|
|
41
51
|
"org:admin": "admin",
|
|
@@ -44,7 +54,7 @@ const CLERK_ORG_ROLE_MAP = {
|
|
|
44
54
|
member: "member",
|
|
45
55
|
"org:client": "client",
|
|
46
56
|
client: "client",
|
|
47
|
-
};
|
|
57
|
+
});
|
|
48
58
|
/**
|
|
49
59
|
* @security ⚠️ DECODE/NORMALIZE-ONLY. NOT AUTHENTICATION. NOT VERIFICATION.
|
|
50
60
|
*
|
|
@@ -114,4 +124,55 @@ export function normalizeVerifiedHumanSession(session) {
|
|
|
114
124
|
}
|
|
115
125
|
return { tenant: orgId, subject: userId, role: mapped };
|
|
116
126
|
}
|
|
127
|
+
function requiredList(required) {
|
|
128
|
+
return typeof required === "string" ? [required] : required;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* THROWING form, for a write boundary. Returns the resolved identity when
|
|
132
|
+
* the verified session carries one of the `required` roles; throws
|
|
133
|
+
* otherwise. Malformed sessions throw exactly what
|
|
134
|
+
* `normalizeVerifiedHumanSession` throws; a well-formed session lacking the
|
|
135
|
+
* role throws "Role refused: ...".
|
|
136
|
+
*
|
|
137
|
+
* Same `@security` precondition as `normalizeVerifiedHumanSession`: this
|
|
138
|
+
* asserts on a session that is ALREADY verified. It verifies nothing.
|
|
139
|
+
*/
|
|
140
|
+
export function requireHumanRole(session, required) {
|
|
141
|
+
const identity = normalizeVerifiedHumanSession(session);
|
|
142
|
+
const allowed = requiredList(required);
|
|
143
|
+
if (!allowed.includes(identity.role)) {
|
|
144
|
+
throw new Error(`Role refused: this session is a verified member but does not carry ${allowed.length === 0 ? "any admitted role" : `role ${allowed.join(" or ")}`}.`);
|
|
145
|
+
}
|
|
146
|
+
return identity;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* NON-THROWING form, for a public read. Returns `{ admitted: true, identity }`
|
|
150
|
+
* or `{ admitted: false, refusal }` where the refusal carries `ROLE_REFUSED`
|
|
151
|
+
* and a reason. Admits exactly the sessions `requireHumanRole` returns for
|
|
152
|
+
* and refuses exactly the ones it throws for.
|
|
153
|
+
*/
|
|
154
|
+
export function resolveHumanRoleOrRefusal(session, required) {
|
|
155
|
+
const refuse = (reason) => ({
|
|
156
|
+
admitted: false,
|
|
157
|
+
refusal: { code: "ROLE_REFUSED", reason },
|
|
158
|
+
});
|
|
159
|
+
if (!session)
|
|
160
|
+
return refuse("no-session");
|
|
161
|
+
const { orgId, userId, orgRole } = session;
|
|
162
|
+
if (typeof orgId !== "string" || orgId.length === 0) {
|
|
163
|
+
return refuse("no-organization");
|
|
164
|
+
}
|
|
165
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
166
|
+
return refuse("no-verified-role");
|
|
167
|
+
}
|
|
168
|
+
if (typeof orgRole !== "string" || orgRole.length === 0) {
|
|
169
|
+
return refuse("no-verified-role");
|
|
170
|
+
}
|
|
171
|
+
const role = CLERK_ORG_ROLE_MAP[orgRole];
|
|
172
|
+
if (!role)
|
|
173
|
+
return refuse("no-verified-role");
|
|
174
|
+
if (!requiredList(required).includes(role))
|
|
175
|
+
return refuse("role-not-held");
|
|
176
|
+
return { admitted: true, identity: { tenant: orgId, subject: userId, role } };
|
|
177
|
+
}
|
|
117
178
|
//# sourceMappingURL=human-path.js.map
|
package/dist/human-path.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"human-path.js","sourceRoot":"","sources":["../src/human-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC;IAC3C,OAAO;IACP,OAAO;IACP,QAAQ;IACR,QAAQ;CACT,CAAC,CAAC;AA6BH;;;;;;GAMG;AACH,MAAM,kBAAkB,GAAqC;
|
|
1
|
+
{"version":3,"file":"human-path.js","sourceRoot":"","sources":["../src/human-path.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* tenant resolution, membership requirement, row filtering and bearer
|
|
6
6
|
* validation.
|
|
7
7
|
*/
|
|
8
|
-
export { timingSafeEqual } from "./crypto.js";
|
|
8
|
+
export { timingSafeEqual, sha256Hex } from "./crypto.js";
|
|
9
9
|
export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, LEGACY_WILDCARD_CTX, type ScopeFilterable, } from "./scope-filter.js";
|
|
10
10
|
export { validateMasterBearer } from "./bearer-validation.js";
|
|
11
11
|
export type { OAuthCtx, ScopeProfile, NamespacePrefix, FromAllowListEntry, ValidateMasterBearerResult, } from "./types.js";
|
|
12
12
|
export { workspaceSchema, workspaceMemberSchema, workspaceRoleSchema, tenantContextSchema, ScopeViolationError, getEffectiveTenantId, decodeUnverifiedBearer, } from "./tenancy-domain.js";
|
|
13
13
|
export type { Workspace, WorkspaceMember, WorkspaceRole, TenantContext, ScopeViolationPayload, BearerPayload, } from "./tenancy-domain.js";
|
|
14
|
-
export { requireTenantId } from "./org-guard.js";
|
|
15
|
-
export type { SessionIdentity, TenantSource, DeploymentMode, } from "./org-guard.js";
|
|
16
|
-
export { humanAccountRoleSchema, normalizeVerifiedHumanSession } from "./human-path.js";
|
|
17
|
-
export type { HumanAccountRole, ClerkSessionLike, ResolvedHumanIdentity, } from "./human-path.js";
|
|
14
|
+
export { requireTenantId, resolveTenantIdOrAbsent } from "./org-guard.js";
|
|
15
|
+
export type { TenantAbsence, TenantAbsenceReason, TenantResolution, SessionIdentity, TenantSource, DeploymentMode, } from "./org-guard.js";
|
|
16
|
+
export { humanAccountRoleSchema, normalizeVerifiedHumanSession, requireHumanRole, resolveHumanRoleOrRefusal, } from "./human-path.js";
|
|
17
|
+
export type { RequiredHumanRole, RoleRefusal, RoleRefusalReason, RoleResolution, HumanAccountRole, ClerkSessionLike, ResolvedHumanIdentity, } from "./human-path.js";
|
|
18
|
+
export { validatePresentedBearer, requireAgentScopedIdentity, storedBearerRowSchema, } from "./presented-bearer.js";
|
|
19
|
+
export type { StoredBearerRow, AgentPresence, ResolvedBearerIdentity, AgentScopedIdentity, ValidatePresentedBearerResult, RequireAgentScopedResult, PresentedBearerDeps, PresentedBearerInternalReason, } from "./presented-bearer.js";
|
|
18
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,SAAS,EACT,eAAe,EACf,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAC1E,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,cAAc,GACf,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAC7B,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,eAAe,EACf,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,6BAA6B,EAC7B,wBAAwB,EACxB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
* tenant resolution, membership requirement, row filtering and bearer
|
|
6
6
|
* validation.
|
|
7
7
|
*/
|
|
8
|
-
export { timingSafeEqual } from "./crypto.js";
|
|
8
|
+
export { timingSafeEqual, sha256Hex } from "./crypto.js";
|
|
9
9
|
export { passesScopeFilter, scopeFilterList, scopeFilterGet, isMasterScope, isWildcardScope, LEGACY_WILDCARD_CTX, } from "./scope-filter.js";
|
|
10
10
|
export { validateMasterBearer } from "./bearer-validation.js";
|
|
11
11
|
// 0.2.0 — Domain-tenancy layer (additive)
|
|
12
12
|
export { workspaceSchema, workspaceMemberSchema, workspaceRoleSchema, tenantContextSchema, ScopeViolationError, getEffectiveTenantId, decodeUnverifiedBearer, } from "./tenancy-domain.js";
|
|
13
13
|
// Organization-membership guard (unified session + bearer + self-host contract)
|
|
14
|
-
export { requireTenantId } from "./org-guard.js";
|
|
14
|
+
export { requireTenantId, resolveTenantIdOrAbsent } from "./org-guard.js";
|
|
15
15
|
// 0.4.0 — Human-path resolver (Clerk-shaped session -> { tenant, subject, role })
|
|
16
|
-
export { humanAccountRoleSchema, normalizeVerifiedHumanSession } from "./human-path.js";
|
|
16
|
+
export { humanAccountRoleSchema, normalizeVerifiedHumanSession, requireHumanRole, resolveHumanRoleOrRefusal, } from "./human-path.js";
|
|
17
|
+
// Presented-bearer resolution — the opaque-token trust boundary for a
|
|
18
|
+
// NON-MASTER credential. The package's single answer to "who is calling"
|
|
19
|
+
// for a presented bearer; consumers must not keep a local variant.
|
|
20
|
+
export { validatePresentedBearer, requireAgentScopedIdentity, storedBearerRowSchema, } from "./presented-bearer.js";
|
|
17
21
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAS9D,0CAA0C;AAC1C,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAU7B,gFAAgF;AAChF,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAU1E,kFAAkF;AAClF,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAC7B,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAWzB,sEAAsE;AACtE,yEAAyE;AACzE,mEAAmE;AACnE,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,uBAAuB,CAAC"}
|
package/dist/org-guard.d.ts
CHANGED
|
@@ -92,4 +92,52 @@ export type TenantSource = {
|
|
|
92
92
|
* organization/workspace information is missing.
|
|
93
93
|
*/
|
|
94
94
|
export declare function requireTenantId(source: TenantSource): string;
|
|
95
|
+
/**
|
|
96
|
+
* Why a caller resolved to no tenant. A closed set, so a consumer can branch
|
|
97
|
+
* on content.
|
|
98
|
+
*/
|
|
99
|
+
export type TenantAbsenceReason = "no-session" | "no-organization" | "no-workspace" | "no-tenant-configured";
|
|
100
|
+
/**
|
|
101
|
+
* The typed absence. It is a NON-empty object carrying its own code: it can
|
|
102
|
+
* never be mistaken for `[]`, `{}`, `null`, `0` or a zeroed aggregate, all of
|
|
103
|
+
* which are what "an organization with nothing in it" looks like.
|
|
104
|
+
*/
|
|
105
|
+
export type TenantAbsence = {
|
|
106
|
+
code: "TENANT_ABSENT";
|
|
107
|
+
reason: TenantAbsenceReason;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Result of `resolveTenantIdOrAbsent`. Branch on `present`; the two arms
|
|
111
|
+
* share no field, so a consumer that forgets to branch gets a type error
|
|
112
|
+
* rather than a silently-empty read.
|
|
113
|
+
*/
|
|
114
|
+
export type TenantResolution = {
|
|
115
|
+
present: true;
|
|
116
|
+
tenantId: string;
|
|
117
|
+
} | {
|
|
118
|
+
present: false;
|
|
119
|
+
absence: TenantAbsence;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* The tenant id is OPAQUE. It is returned byte-for-byte as presented
|
|
123
|
+
* (`orgId`, `workspaceId` or the self-host `tenantId`), never trimmed,
|
|
124
|
+
* lower-cased or otherwise interpreted. This package never receives or
|
|
125
|
+
* derives a slug or any human-facing name; each product maps its own notion
|
|
126
|
+
* (an organization id, a workspace id) onto that string.
|
|
127
|
+
*
|
|
128
|
+
* NON-THROWING sibling of `requireTenantId`, for a public READ.
|
|
129
|
+
*
|
|
130
|
+
* `requireTenantId` throws on every branch, which is right for a WRITE
|
|
131
|
+
* boundary and wrong for a subscribed read: a throw at a caller with no
|
|
132
|
+
* organization crashes a mounted render, while a bare empty value is
|
|
133
|
+
* byte-identical to "there is nothing". This function does neither: it
|
|
134
|
+
* returns the tenant, or a typed absence that cannot be confused with an
|
|
135
|
+
* empty result.
|
|
136
|
+
*
|
|
137
|
+
* Same `TenantSource` union, same refusal conditions as `requireTenantId` —
|
|
138
|
+
* every input that makes it throw makes this return `{ present: false }`,
|
|
139
|
+
* and every input it serves is served identically. Grants nothing that
|
|
140
|
+
* `requireTenantId` refuses.
|
|
141
|
+
*/
|
|
142
|
+
export declare function resolveTenantIdOrAbsent(source: TenantSource): TenantResolution;
|
|
95
143
|
//# sourceMappingURL=org-guard.d.ts.map
|
package/dist/org-guard.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-guard.d.ts","sourceRoot":"","sources":["../src/org-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA+B5D"}
|
|
1
|
+
{"version":3,"file":"org-guard.d.ts","sourceRoot":"","sources":["../src/org-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA+B5D;AAMD;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,iBAAiB,GACjB,cAAc,GACd,sBAAsB,CAAC;AAE3B;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,GAAG,gBAAgB,CA4B9E"}
|
package/dist/org-guard.js
CHANGED
|
@@ -74,4 +74,52 @@ export function requireTenantId(source) {
|
|
|
74
74
|
}
|
|
75
75
|
return workspaceId;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The tenant id is OPAQUE. It is returned byte-for-byte as presented
|
|
79
|
+
* (`orgId`, `workspaceId` or the self-host `tenantId`), never trimmed,
|
|
80
|
+
* lower-cased or otherwise interpreted. This package never receives or
|
|
81
|
+
* derives a slug or any human-facing name; each product maps its own notion
|
|
82
|
+
* (an organization id, a workspace id) onto that string.
|
|
83
|
+
*
|
|
84
|
+
* NON-THROWING sibling of `requireTenantId`, for a public READ.
|
|
85
|
+
*
|
|
86
|
+
* `requireTenantId` throws on every branch, which is right for a WRITE
|
|
87
|
+
* boundary and wrong for a subscribed read: a throw at a caller with no
|
|
88
|
+
* organization crashes a mounted render, while a bare empty value is
|
|
89
|
+
* byte-identical to "there is nothing". This function does neither: it
|
|
90
|
+
* returns the tenant, or a typed absence that cannot be confused with an
|
|
91
|
+
* empty result.
|
|
92
|
+
*
|
|
93
|
+
* Same `TenantSource` union, same refusal conditions as `requireTenantId` —
|
|
94
|
+
* every input that makes it throw makes this return `{ present: false }`,
|
|
95
|
+
* and every input it serves is served identically. Grants nothing that
|
|
96
|
+
* `requireTenantId` refuses.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveTenantIdOrAbsent(source) {
|
|
99
|
+
const absent = (reason) => ({
|
|
100
|
+
present: false,
|
|
101
|
+
absence: { code: "TENANT_ABSENT", reason },
|
|
102
|
+
});
|
|
103
|
+
if (source.kind === "session") {
|
|
104
|
+
if (!source.identity)
|
|
105
|
+
return absent("no-session");
|
|
106
|
+
const orgId = source.identity.orgId;
|
|
107
|
+
if (typeof orgId !== "string" || orgId.length === 0) {
|
|
108
|
+
return absent("no-organization");
|
|
109
|
+
}
|
|
110
|
+
return { present: true, tenantId: orgId };
|
|
111
|
+
}
|
|
112
|
+
if (source.kind === "self-host") {
|
|
113
|
+
const tenantId = source.tenantId;
|
|
114
|
+
if (typeof tenantId !== "string" || tenantId.length === 0) {
|
|
115
|
+
return absent("no-tenant-configured");
|
|
116
|
+
}
|
|
117
|
+
return { present: true, tenantId };
|
|
118
|
+
}
|
|
119
|
+
const workspaceId = source.context?.workspaceId;
|
|
120
|
+
if (typeof workspaceId !== "string" || workspaceId.length === 0) {
|
|
121
|
+
return absent("no-workspace");
|
|
122
|
+
}
|
|
123
|
+
return { present: true, tenantId: workspaceId };
|
|
124
|
+
}
|
|
77
125
|
//# sourceMappingURL=org-guard.js.map
|
package/dist/org-guard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-guard.js","sourceRoot":"","sources":["../src/org-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAyCH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,MAAoB;IAClD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CACb,yFAAyF,CAC1F,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,KAAK,CACb,gHAAgH,CACjH,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC;IAChD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
|
1
|
+
{"version":3,"file":"org-guard.js","sourceRoot":"","sources":["../src/org-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAyCH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,MAAoB;IAClD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CACb,yFAAyF,CAC1F,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,KAAK,CACb,gHAAgH,CACjH,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC;IAChD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAmCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAoB;IAC1D,MAAM,MAAM,GAAG,CAAC,MAA2B,EAAoB,EAAE,CAAC,CAAC;QACjE,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE;KAC3C,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC;QAClD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,OAAO,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC5C,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1D,OAAO,MAAM,CAAC,sBAAsB,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACrC,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC;IAChD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,OAAO,MAAM,CAAC,cAAc,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presented-bearer resolution — the opaque-token trust boundary.
|
|
3
|
+
* (See package.json "version" for the release this shipped in.)
|
|
4
|
+
*
|
|
5
|
+
* Closes the gap between the two credential paths the package already had:
|
|
6
|
+
*
|
|
7
|
+
* - `validateMasterBearer` covers the MASTER credential only (one configured
|
|
8
|
+
* secret, no datastore, no identity beyond "it was the master").
|
|
9
|
+
* - `decodeUnverifiedBearer` says in its own name that it verifies nothing;
|
|
10
|
+
* its payload is attacker-controlled and is not a trust boundary.
|
|
11
|
+
*
|
|
12
|
+
* Between them there was no function taking a PRESENTED, NON-MASTER bearer,
|
|
13
|
+
* hashing it, looking the row up and returning a resolved identity or a typed
|
|
14
|
+
* refusal. Consumers therefore wrote that primitive locally — a second
|
|
15
|
+
* authority answering "who is calling", which is exactly what must not exist.
|
|
16
|
+
* This module is the single one.
|
|
17
|
+
*
|
|
18
|
+
* DIVISION OF LABOUR. The package cannot query a datastore, so the row lookup
|
|
19
|
+
* arrives as a caller-supplied callback. Everything that can be got wrong
|
|
20
|
+
* stays here: the header parse, the hashing, the constant-time digest compare,
|
|
21
|
+
* revocation, expiry, row-shape validation, and the SHAPE of the refusal. The
|
|
22
|
+
* caller supplies one indexed read and nothing else.
|
|
23
|
+
*
|
|
24
|
+
* Errors are coarse-grained on purpose — callers should not surface the exact
|
|
25
|
+
* reason to remote clients beyond a generic 401/403. The vocabulary is
|
|
26
|
+
* `validateMasterBearer`'s, unchanged: "missing", "malformed", "mismatch".
|
|
27
|
+
*
|
|
28
|
+
* @security The callback is invoked with the SHA-256 HEX DIGEST of the
|
|
29
|
+
* presented token, never the token. A consumer's datastore query, query log
|
|
30
|
+
* and slow-query log therefore never see a usable secret. The raw token does
|
|
31
|
+
* not appear in any returned value, any refusal, or any logging callback.
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
/**
|
|
35
|
+
* Shape of the row a consumer's datastore returns for a given digest.
|
|
36
|
+
*
|
|
37
|
+
* `agentId` ABSENT (undefined or null) means an ORGANIZATION-WIDE token: it
|
|
38
|
+
* carries a tenant but no agent identity. Such a row is valid and resolves
|
|
39
|
+
* successfully; what it may NOT do is stand in for an agent. See
|
|
40
|
+
* `requireAgentScopedIdentity`.
|
|
41
|
+
*
|
|
42
|
+
* `secretHash` is the digest as stored by the consumer. It is re-compared here
|
|
43
|
+
* against the digest computed from the presented token, in constant time, so a
|
|
44
|
+
* lookup that performs a loose match (prefix, LIKE, case-folded index) or that
|
|
45
|
+
* returns the wrong row cannot become an authentication.
|
|
46
|
+
*/
|
|
47
|
+
export declare const storedBearerRowSchema: z.ZodObject<{
|
|
48
|
+
tenantId: z.ZodString;
|
|
49
|
+
secretHash: z.ZodString;
|
|
50
|
+
agentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
51
|
+
revoked: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
52
|
+
expiresAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
53
|
+
scopes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
export type StoredBearerRow = z.infer<typeof storedBearerRowSchema>;
|
|
56
|
+
/**
|
|
57
|
+
* The agent, present or explicitly absent. A DISCRIMINATED UNION rather than a
|
|
58
|
+
* nullable `agentId?: string`, because a nullable field is a duty to remember
|
|
59
|
+
* and this one was being forgotten: `identity.agent.agentId` does not type-check
|
|
60
|
+
* until the caller has narrowed on `present`.
|
|
61
|
+
*/
|
|
62
|
+
export type AgentPresence = {
|
|
63
|
+
present: true;
|
|
64
|
+
agentId: string;
|
|
65
|
+
} | {
|
|
66
|
+
present: false;
|
|
67
|
+
code: "AGENT_ABSENT";
|
|
68
|
+
reason: "organization-wide-token";
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A resolved, verified caller. Carries the tenant always, and the agent as a
|
|
72
|
+
* presence-or-absence. Carries NO secret and no digest.
|
|
73
|
+
*/
|
|
74
|
+
export type ResolvedBearerIdentity = {
|
|
75
|
+
tenantId: string;
|
|
76
|
+
agent: AgentPresence;
|
|
77
|
+
scopes: readonly string[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Brand for `AgentScopedIdentity`. Declared and NOT exported, so the branded
|
|
81
|
+
* type cannot be constructed anywhere outside this module: a per-agent surface
|
|
82
|
+
* that types its parameter as `AgentScopedIdentity` can only be reached through
|
|
83
|
+
* `requireAgentScopedIdentity`. That is what makes the organization-wide-token
|
|
84
|
+
* refusal structural rather than a convention a caller may skip.
|
|
85
|
+
*/
|
|
86
|
+
declare const agentScopedBrand: unique symbol;
|
|
87
|
+
/**
|
|
88
|
+
* A resolved caller that HAS an agent. `agentId` is a plain non-optional
|
|
89
|
+
* string, so per-agent code needs no narrowing and has nothing to forget.
|
|
90
|
+
*/
|
|
91
|
+
export type AgentScopedIdentity = {
|
|
92
|
+
readonly [agentScopedBrand]: true;
|
|
93
|
+
tenantId: string;
|
|
94
|
+
agentId: string;
|
|
95
|
+
scopes: readonly string[];
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Result of `validatePresentedBearer`.
|
|
99
|
+
*
|
|
100
|
+
* - `ok: true` → the row was found, its digest matched in constant time, it
|
|
101
|
+
* is neither revoked nor expired, and its shape is valid.
|
|
102
|
+
* - `error: "missing"` → no Authorization header.
|
|
103
|
+
* - `error: "malformed"` → header present but not parseable as
|
|
104
|
+
* `Bearer <non-empty-token>`.
|
|
105
|
+
* - `error: "mismatch"` → the single, COLLAPSED refusal. An unknown digest,
|
|
106
|
+
* a revoked row, an expired row, a row of invalid shape and a row whose
|
|
107
|
+
* stored digest does not match all produce this value and nothing else, so
|
|
108
|
+
* a prober cannot learn from the response whether a token was ever real.
|
|
109
|
+
* - `error: "unavailable"` → the lookup callback itself threw or rejected.
|
|
110
|
+
* Deliberately NOT in the collapsed bucket: a caller must be able to
|
|
111
|
+
* answer 503 rather than 401 when its datastore is down, and an attacker
|
|
112
|
+
* cannot induce this state for one chosen token rather than another, so it
|
|
113
|
+
* discloses nothing about any token. It is a REFUSAL like the others — a
|
|
114
|
+
* lookup failure never falls through to a grant.
|
|
115
|
+
*/
|
|
116
|
+
export type ValidatePresentedBearerResult = {
|
|
117
|
+
ok: true;
|
|
118
|
+
identity: ResolvedBearerIdentity;
|
|
119
|
+
error?: undefined;
|
|
120
|
+
} | {
|
|
121
|
+
ok: false;
|
|
122
|
+
error: "missing" | "malformed" | "mismatch" | "unavailable";
|
|
123
|
+
identity?: undefined;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Result of `requireAgentScopedIdentity`. The refusal is distinguishable from
|
|
127
|
+
* `validatePresentedBearer`'s refusals on purpose: the credential here is
|
|
128
|
+
* known-good, and the caller owes the remote client a 403 (wrong surface for
|
|
129
|
+
* this token), not a 401 (bad token).
|
|
130
|
+
*/
|
|
131
|
+
export type RequireAgentScopedResult = {
|
|
132
|
+
ok: true;
|
|
133
|
+
identity: AgentScopedIdentity;
|
|
134
|
+
error?: undefined;
|
|
135
|
+
} | {
|
|
136
|
+
ok: false;
|
|
137
|
+
error: "no-agent-on-token";
|
|
138
|
+
identity?: undefined;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Why a refusal happened, for the consumer's own observability. Reported
|
|
142
|
+
* through `deps.onRefusal`; NEVER present in a returned value, because a
|
|
143
|
+
* returned distinction is a returned disclosure.
|
|
144
|
+
*
|
|
145
|
+
* @security Carries no token, no digest and no row. Only a reason word.
|
|
146
|
+
*/
|
|
147
|
+
export type PresentedBearerInternalReason = "no-header" | "malformed-header" | "unknown-token" | "revoked-token" | "expired-token" | "row-shape-invalid" | "stored-digest-mismatch" | "lookup-failed";
|
|
148
|
+
export type PresentedBearerDeps = {
|
|
149
|
+
/**
|
|
150
|
+
* Indexed read of one row by the SHA-256 HEX DIGEST of the presented token.
|
|
151
|
+
* Returns the row, or null/undefined when no row carries that digest.
|
|
152
|
+
*
|
|
153
|
+
* @security The argument is a digest, never the token. Do not log it, and do
|
|
154
|
+
* not widen this to accept a raw secret.
|
|
155
|
+
*/
|
|
156
|
+
lookupBySecretHash: (sha256HexDigest: string) => Promise<StoredBearerRow | null | undefined>;
|
|
157
|
+
/** Injectable clock. Never `Date.now()` inline, so expiry is testable. */
|
|
158
|
+
now?: () => number;
|
|
159
|
+
/** Observability sink for the collapsed refusals. Must not throw; if it does, the throw is swallowed and the refusal stands. */
|
|
160
|
+
onRefusal?: (reason: PresentedBearerInternalReason) => void;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Resolve an `Authorization: Bearer <token>` header presented by a NON-MASTER
|
|
164
|
+
* caller into a verified identity, or into a typed refusal.
|
|
165
|
+
*
|
|
166
|
+
* Order of operations, and why:
|
|
167
|
+
* 1. Parse the header. A missing or malformed header is refused WITHOUT
|
|
168
|
+
* calling the lookup, so a garbage request costs no datastore read.
|
|
169
|
+
* 2. Hash the token to a SHA-256 hex digest. The raw token goes no further.
|
|
170
|
+
* 3. Look the row up BY THAT DIGEST.
|
|
171
|
+
* 4. Re-compare the row's stored digest against the computed one with
|
|
172
|
+
* `timingSafeEqual` — never `!==` on a secret or a digest.
|
|
173
|
+
* 5. Validate the row's shape, then revocation, then expiry.
|
|
174
|
+
* 6. Resolve the tenant, and the agent as a presence or an explicit absence.
|
|
175
|
+
*
|
|
176
|
+
* Steps 3–5 all refuse with the SAME frozen value. A right is never granted by
|
|
177
|
+
* absence or by failure: there is no branch that returns `ok: true` without
|
|
178
|
+
* having passed every check above.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* const result = await validatePresentedBearer(req.headers.get("authorization"), {
|
|
183
|
+
* lookupBySecretHash: (digest) => db.tokens.byDigest(digest),
|
|
184
|
+
* });
|
|
185
|
+
* if (!result.ok) return respond(result.error === "unavailable" ? 503 : 401);
|
|
186
|
+
* const tenantId = result.identity.tenantId;
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export declare function validatePresentedBearer(authHeader: string | null | undefined, deps: PresentedBearerDeps): Promise<ValidatePresentedBearerResult>;
|
|
190
|
+
/**
|
|
191
|
+
* Narrow a resolved identity to a PER-AGENT surface, refusing when the token
|
|
192
|
+
* carries no agent.
|
|
193
|
+
*
|
|
194
|
+
* An organization-wide token is a legitimate credential that is simply not an
|
|
195
|
+
* agent; treating it as one would silently widen it into whichever agent the
|
|
196
|
+
* surface happened to be about. So this is a second, mandatory call, and its
|
|
197
|
+
* output type is branded: a per-agent handler declares `AgentScopedIdentity`
|
|
198
|
+
* and the only way to obtain one is here.
|
|
199
|
+
*
|
|
200
|
+
* Grants nothing on its own — it can only ever narrow an identity
|
|
201
|
+
* `validatePresentedBearer` already resolved.
|
|
202
|
+
*/
|
|
203
|
+
export declare function requireAgentScopedIdentity(identity: ResolvedBearerIdentity): RequireAgentScopedResult;
|
|
204
|
+
export {};
|
|
205
|
+
//# sourceMappingURL=presented-bearer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presented-bearer.d.ts","sourceRoot":"","sources":["../src/presented-bearer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB;;;;;;;iBAOhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAMpE;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAA;CAAE,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,CAAC;AAEF;;;;;;GAMG;AACH,OAAO,CAAC,MAAM,gBAAgB,EAAE,OAAO,MAAM,CAAC;AAE9C;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,CAAC,gBAAgB,CAAC,EAAE,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,CAAC;AAMF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,sBAAsB,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GACjE;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,aAAa,CAAC;IAC5D,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEN;;;;;GAKG;AACH,MAAM,MAAM,wBAAwB,GAChC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GAC9D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AAMpE;;;;;;GAMG;AACH,MAAM,MAAM,6BAA6B,GACrC,WAAW,GACX,kBAAkB,GAClB,eAAe,GACf,eAAe,GACf,eAAe,GACf,mBAAmB,GACnB,wBAAwB,GACxB,eAAe,CAAC;AAEpB,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;;;OAMG;IACH,kBAAkB,EAAE,CAClB,eAAe,EAAE,MAAM,KACpB,OAAO,CAAC,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACjD,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,gIAAgI;IAChI,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,6BAA6B,KAAK,IAAI,CAAC;CAC7D,CAAC;AAyDF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACrC,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,6BAA6B,CAAC,CAkFxC;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,sBAAsB,GAC/B,wBAAwB,CAY1B"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presented-bearer resolution — the opaque-token trust boundary.
|
|
3
|
+
* (See package.json "version" for the release this shipped in.)
|
|
4
|
+
*
|
|
5
|
+
* Closes the gap between the two credential paths the package already had:
|
|
6
|
+
*
|
|
7
|
+
* - `validateMasterBearer` covers the MASTER credential only (one configured
|
|
8
|
+
* secret, no datastore, no identity beyond "it was the master").
|
|
9
|
+
* - `decodeUnverifiedBearer` says in its own name that it verifies nothing;
|
|
10
|
+
* its payload is attacker-controlled and is not a trust boundary.
|
|
11
|
+
*
|
|
12
|
+
* Between them there was no function taking a PRESENTED, NON-MASTER bearer,
|
|
13
|
+
* hashing it, looking the row up and returning a resolved identity or a typed
|
|
14
|
+
* refusal. Consumers therefore wrote that primitive locally — a second
|
|
15
|
+
* authority answering "who is calling", which is exactly what must not exist.
|
|
16
|
+
* This module is the single one.
|
|
17
|
+
*
|
|
18
|
+
* DIVISION OF LABOUR. The package cannot query a datastore, so the row lookup
|
|
19
|
+
* arrives as a caller-supplied callback. Everything that can be got wrong
|
|
20
|
+
* stays here: the header parse, the hashing, the constant-time digest compare,
|
|
21
|
+
* revocation, expiry, row-shape validation, and the SHAPE of the refusal. The
|
|
22
|
+
* caller supplies one indexed read and nothing else.
|
|
23
|
+
*
|
|
24
|
+
* Errors are coarse-grained on purpose — callers should not surface the exact
|
|
25
|
+
* reason to remote clients beyond a generic 401/403. The vocabulary is
|
|
26
|
+
* `validateMasterBearer`'s, unchanged: "missing", "malformed", "mismatch".
|
|
27
|
+
*
|
|
28
|
+
* @security The callback is invoked with the SHA-256 HEX DIGEST of the
|
|
29
|
+
* presented token, never the token. A consumer's datastore query, query log
|
|
30
|
+
* and slow-query log therefore never see a usable secret. The raw token does
|
|
31
|
+
* not appear in any returned value, any refusal, or any logging callback.
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { sha256Hex, timingSafeEqual } from "./crypto.js";
|
|
35
|
+
const BEARER_PREFIX_LOWER = "bearer ";
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// The stored row
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* Shape of the row a consumer's datastore returns for a given digest.
|
|
41
|
+
*
|
|
42
|
+
* `agentId` ABSENT (undefined or null) means an ORGANIZATION-WIDE token: it
|
|
43
|
+
* carries a tenant but no agent identity. Such a row is valid and resolves
|
|
44
|
+
* successfully; what it may NOT do is stand in for an agent. See
|
|
45
|
+
* `requireAgentScopedIdentity`.
|
|
46
|
+
*
|
|
47
|
+
* `secretHash` is the digest as stored by the consumer. It is re-compared here
|
|
48
|
+
* against the digest computed from the presented token, in constant time, so a
|
|
49
|
+
* lookup that performs a loose match (prefix, LIKE, case-folded index) or that
|
|
50
|
+
* returns the wrong row cannot become an authentication.
|
|
51
|
+
*/
|
|
52
|
+
export const storedBearerRowSchema = z.object({
|
|
53
|
+
tenantId: z.string().min(1),
|
|
54
|
+
secretHash: z.string().min(1),
|
|
55
|
+
agentId: z.string().min(1).nullish(),
|
|
56
|
+
revoked: z.boolean().nullish(),
|
|
57
|
+
expiresAt: z.number().nullish(),
|
|
58
|
+
scopes: z.array(z.string()).nullish(),
|
|
59
|
+
});
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// The refusals, as shared frozen singletons
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
const MISSING = Object.freeze({
|
|
64
|
+
ok: false,
|
|
65
|
+
error: "missing",
|
|
66
|
+
});
|
|
67
|
+
const MALFORMED = Object.freeze({
|
|
68
|
+
ok: false,
|
|
69
|
+
error: "malformed",
|
|
70
|
+
});
|
|
71
|
+
const UNAVAILABLE = Object.freeze({
|
|
72
|
+
ok: false,
|
|
73
|
+
error: "unavailable",
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* THE collapsed refusal. One frozen value, shared by every branch that must be
|
|
77
|
+
* externally indistinguishable — unknown digest, revoked, expired, invalid row
|
|
78
|
+
* shape, stored-digest mismatch. Not five literals that happen to be equal
|
|
79
|
+
* today: one object, so the branches cannot drift apart by an added field.
|
|
80
|
+
*/
|
|
81
|
+
const COLLAPSED_REFUSAL = Object.freeze({
|
|
82
|
+
ok: false,
|
|
83
|
+
error: "mismatch",
|
|
84
|
+
});
|
|
85
|
+
function report(deps, reason) {
|
|
86
|
+
const sink = deps.onRefusal;
|
|
87
|
+
if (typeof sink !== "function")
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
sink(reason);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// A failing logger must never change an authorization outcome.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function collapse(deps, reason) {
|
|
97
|
+
report(deps, reason);
|
|
98
|
+
return COLLAPSED_REFUSAL;
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// validatePresentedBearer
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/**
|
|
104
|
+
* Resolve an `Authorization: Bearer <token>` header presented by a NON-MASTER
|
|
105
|
+
* caller into a verified identity, or into a typed refusal.
|
|
106
|
+
*
|
|
107
|
+
* Order of operations, and why:
|
|
108
|
+
* 1. Parse the header. A missing or malformed header is refused WITHOUT
|
|
109
|
+
* calling the lookup, so a garbage request costs no datastore read.
|
|
110
|
+
* 2. Hash the token to a SHA-256 hex digest. The raw token goes no further.
|
|
111
|
+
* 3. Look the row up BY THAT DIGEST.
|
|
112
|
+
* 4. Re-compare the row's stored digest against the computed one with
|
|
113
|
+
* `timingSafeEqual` — never `!==` on a secret or a digest.
|
|
114
|
+
* 5. Validate the row's shape, then revocation, then expiry.
|
|
115
|
+
* 6. Resolve the tenant, and the agent as a presence or an explicit absence.
|
|
116
|
+
*
|
|
117
|
+
* Steps 3–5 all refuse with the SAME frozen value. A right is never granted by
|
|
118
|
+
* absence or by failure: there is no branch that returns `ok: true` without
|
|
119
|
+
* having passed every check above.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const result = await validatePresentedBearer(req.headers.get("authorization"), {
|
|
124
|
+
* lookupBySecretHash: (digest) => db.tokens.byDigest(digest),
|
|
125
|
+
* });
|
|
126
|
+
* if (!result.ok) return respond(result.error === "unavailable" ? 503 : 401);
|
|
127
|
+
* const tenantId = result.identity.tenantId;
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export async function validatePresentedBearer(authHeader, deps) {
|
|
131
|
+
if (typeof deps?.lookupBySecretHash !== "function") {
|
|
132
|
+
// A programming error in the consumer, not a claim about any caller: it
|
|
133
|
+
// must not be reported as a credential refusal.
|
|
134
|
+
throw new TypeError("validatePresentedBearer: deps.lookupBySecretHash must be a function.");
|
|
135
|
+
}
|
|
136
|
+
if (authHeader === undefined || authHeader === null || authHeader === "") {
|
|
137
|
+
report(deps, "no-header");
|
|
138
|
+
return MISSING;
|
|
139
|
+
}
|
|
140
|
+
// Case-insensitive scheme match without lower-casing the token bytes
|
|
141
|
+
// (RFC 7235 §2.1), identical to `validateMasterBearer`.
|
|
142
|
+
const lower = authHeader.toLowerCase();
|
|
143
|
+
if (!lower.startsWith(BEARER_PREFIX_LOWER)) {
|
|
144
|
+
report(deps, "malformed-header");
|
|
145
|
+
return MALFORMED;
|
|
146
|
+
}
|
|
147
|
+
const token = authHeader.slice(BEARER_PREFIX_LOWER.length).trim();
|
|
148
|
+
if (token.length === 0) {
|
|
149
|
+
report(deps, "malformed-header");
|
|
150
|
+
return MALFORMED;
|
|
151
|
+
}
|
|
152
|
+
const presentedDigest = await sha256Hex(token);
|
|
153
|
+
let row;
|
|
154
|
+
try {
|
|
155
|
+
row = await deps.lookupBySecretHash(presentedDigest);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
report(deps, "lookup-failed");
|
|
159
|
+
return UNAVAILABLE;
|
|
160
|
+
}
|
|
161
|
+
if (row === null || row === undefined) {
|
|
162
|
+
return collapse(deps, "unknown-token");
|
|
163
|
+
}
|
|
164
|
+
const parsed = storedBearerRowSchema.safeParse(row);
|
|
165
|
+
if (!parsed.success) {
|
|
166
|
+
return collapse(deps, "row-shape-invalid");
|
|
167
|
+
}
|
|
168
|
+
const stored = parsed.data;
|
|
169
|
+
const encoder = new TextEncoder();
|
|
170
|
+
const digestsMatch = await timingSafeEqual(encoder.encode(stored.secretHash.toLowerCase()), encoder.encode(presentedDigest));
|
|
171
|
+
if (!digestsMatch) {
|
|
172
|
+
return collapse(deps, "stored-digest-mismatch");
|
|
173
|
+
}
|
|
174
|
+
if (stored.revoked === true) {
|
|
175
|
+
return collapse(deps, "revoked-token");
|
|
176
|
+
}
|
|
177
|
+
const nowMs = deps.now ? deps.now() : Date.now();
|
|
178
|
+
if (typeof stored.expiresAt === "number" && stored.expiresAt <= nowMs) {
|
|
179
|
+
return collapse(deps, "expired-token");
|
|
180
|
+
}
|
|
181
|
+
const agent = typeof stored.agentId === "string" && stored.agentId.length > 0
|
|
182
|
+
? { present: true, agentId: stored.agentId }
|
|
183
|
+
: {
|
|
184
|
+
present: false,
|
|
185
|
+
code: "AGENT_ABSENT",
|
|
186
|
+
reason: "organization-wide-token",
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
identity: {
|
|
191
|
+
tenantId: stored.tenantId,
|
|
192
|
+
agent,
|
|
193
|
+
scopes: stored.scopes ?? [],
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// requireAgentScopedIdentity
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
/**
|
|
201
|
+
* Narrow a resolved identity to a PER-AGENT surface, refusing when the token
|
|
202
|
+
* carries no agent.
|
|
203
|
+
*
|
|
204
|
+
* An organization-wide token is a legitimate credential that is simply not an
|
|
205
|
+
* agent; treating it as one would silently widen it into whichever agent the
|
|
206
|
+
* surface happened to be about. So this is a second, mandatory call, and its
|
|
207
|
+
* output type is branded: a per-agent handler declares `AgentScopedIdentity`
|
|
208
|
+
* and the only way to obtain one is here.
|
|
209
|
+
*
|
|
210
|
+
* Grants nothing on its own — it can only ever narrow an identity
|
|
211
|
+
* `validatePresentedBearer` already resolved.
|
|
212
|
+
*/
|
|
213
|
+
export function requireAgentScopedIdentity(identity) {
|
|
214
|
+
if (!identity.agent.present) {
|
|
215
|
+
return { ok: false, error: "no-agent-on-token" };
|
|
216
|
+
}
|
|
217
|
+
const scoped = {
|
|
218
|
+
tenantId: identity.tenantId,
|
|
219
|
+
agentId: identity.agent.agentId,
|
|
220
|
+
scopes: identity.scopes,
|
|
221
|
+
// The brand exists only in the type system; there is no runtime field to
|
|
222
|
+
// forge, and no value to construct outside this module.
|
|
223
|
+
};
|
|
224
|
+
return { ok: true, identity: scoped };
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=presented-bearer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presented-bearer.js","sourceRoot":"","sources":["../src/presented-bearer.ts"],"names":[],"mappings":"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"}
|
package/dist/tenancy-domain.d.ts
CHANGED
|
@@ -105,9 +105,12 @@ export declare function getEffectiveTenantId(ctx: TenantContext, args: {
|
|
|
105
105
|
* - decoder for a bearer that was ALREADY verified by an upstream signed-JWT
|
|
106
106
|
* middleware (the production trust boundary)
|
|
107
107
|
*
|
|
108
|
-
* For the production trust boundary, callers MUST
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* For the production trust boundary on an OPAQUE token, callers MUST use
|
|
109
|
+
* `validatePresentedBearer` (see `./presented-bearer.ts`), which hashes the
|
|
110
|
+
* presented token, looks the row up by digest through a caller-supplied read,
|
|
111
|
+
* re-compares the stored digest in constant time, and honours revocation and
|
|
112
|
+
* expiry. For a SIGNED JWT, callers must substitute a signature verifier; this
|
|
113
|
+
* package does not export one.
|
|
111
114
|
*
|
|
112
115
|
* Renamed from `resolveBearer` (0.2.0 pre-release) to make the lack of
|
|
113
116
|
* verification impossible to miss at the call-site.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenancy-domain.d.ts","sourceRoot":"","sources":["../src/tenancy-domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,mBAAmB;;;;EAAwC,CAAC;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,eAAO,MAAM,eAAe;;;;;iBAK1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAMxD,eAAO,MAAM,qBAAqB;;;;;;;;;iBAKhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAMpE,eAAO,MAAM,mBAAmB;;;;;;;;iBAI9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAG,iBAAiB,CAAU;IAC3C,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;gBAE5B,OAAO,EAAE,qBAAqB;CAS3C;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5B,MAAM,CASR;AAMD
|
|
1
|
+
{"version":3,"file":"tenancy-domain.d.ts","sourceRoot":"","sources":["../src/tenancy-domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,mBAAmB;;;;EAAwC,CAAC;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,eAAO,MAAM,eAAe;;;;;iBAK1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAMxD,eAAO,MAAM,qBAAqB;;;;;;;;;iBAKhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAMpE,eAAO,MAAM,mBAAmB;;;;;;;;iBAI9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAG,iBAAiB,CAAU;IAC3C,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;gBAE5B,OAAO,EAAE,qBAAqB;CAS3C;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5B,MAAM,CASR;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,QAAA,MAAM,mBAAmB;;;;;;;;iBAIvB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,CAAC,CAmBxB"}
|
package/dist/tenancy-domain.js
CHANGED
|
@@ -115,9 +115,12 @@ export function getEffectiveTenantId(ctx, args) {
|
|
|
115
115
|
* - decoder for a bearer that was ALREADY verified by an upstream signed-JWT
|
|
116
116
|
* middleware (the production trust boundary)
|
|
117
117
|
*
|
|
118
|
-
* For the production trust boundary, callers MUST
|
|
119
|
-
*
|
|
120
|
-
*
|
|
118
|
+
* For the production trust boundary on an OPAQUE token, callers MUST use
|
|
119
|
+
* `validatePresentedBearer` (see `./presented-bearer.ts`), which hashes the
|
|
120
|
+
* presented token, looks the row up by digest through a caller-supplied read,
|
|
121
|
+
* re-compares the stored digest in constant time, and honours revocation and
|
|
122
|
+
* expiry. For a SIGNED JWT, callers must substitute a signature verifier; this
|
|
123
|
+
* package does not export one.
|
|
121
124
|
*
|
|
122
125
|
* Renamed from `resolveBearer` (0.2.0 pre-release) to make the lack of
|
|
123
126
|
* verification impossible to miss at the call-site.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenancy-domain.js","sourceRoot":"","sources":["../src/tenancy-domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAGzE,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;CACvD,CAAC,CAAC;AAGH,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,IAAI,EAAE,mBAAmB;IACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;CACrB,CAAC,CAAC;AAGH,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC;CACpC,CAAC,CAAC;AAaH;;;;;;GAMG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACnC,IAAI,GAAG,iBAA0B,CAAC;IAClC,OAAO,CAAwB;IAExC,YAAY,OAA8B;QACxC,KAAK,CACH,sCAAsC,OAAO,CAAC,iBAAiB,oCAAoC,OAAO,CAAC,eAAe,OAAO,OAAO,CAAC,MAAM,EAAE,CAClJ,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,kFAAkF;QAClF,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED,8EAA8E;AAC9E,uBAAuB;AACvB,8EAA8E;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAkB,EAClB,IAA6B;IAE7B,IAAI,IAAI,CAAC,WAAW,KAAK,GAAG,CAAC,WAAW,EAAE,CAAC;QACzC,MAAM,IAAI,mBAAmB,CAAC;YAC5B,iBAAiB,EAAE,IAAI,CAAC,WAAW;YACnC,eAAe,EAAE,GAAG,CAAC,WAAW;YAChC,MAAM,EAAE,qEAAqE;SAC9E,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC,WAAW,CAAC;AACzB,CAAC;AAED,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E
|
|
1
|
+
{"version":3,"file":"tenancy-domain.js","sourceRoot":"","sources":["../src/tenancy-domain.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantageos/cloud-identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Framework-agnostic identity + scope-filter primitives for VantagePeers Cloud (multi-tenant MCP).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"./human-path": {
|
|
34
34
|
"types": "./dist/human-path.d.ts",
|
|
35
35
|
"import": "./dist/human-path.js"
|
|
36
|
+
},
|
|
37
|
+
"./presented-bearer": {
|
|
38
|
+
"types": "./dist/presented-bearer.d.ts",
|
|
39
|
+
"import": "./dist/presented-bearer.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"files": [
|