@vantageos/cloud-identity 0.8.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -3,9 +3,40 @@
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
- ## [Unreleased]
6
+ ## [0.9.1]
7
7
 
8
- **Additive — authorize a PERSON and bind an organisation (proposed 0.8.0).**
8
+ ### Fixed
9
+
10
+ - `resolveWriterRole` / `requireWriterRole`: an absent `writerRoles` list (a
11
+ consumer that failed to load it) now returns the typed `role-not-writer`
12
+ refusal instead of throwing a `TypeError`. It already granted nothing; the
13
+ refusal now carries its code, as the JSDoc and test promised. Found by the
14
+ reviewer of #15.
15
+
16
+ ## [0.9.0]
17
+
18
+ **Additive — the person principal (`./person-principal`).** The decisions a
19
+ server needs to serve a signed-in person in its own name, extracted from the
20
+ first consumer so that every other one wires them the same way.
21
+
22
+ ### Added
23
+
24
+ - `resolvePersonPrincipal`: a presented token's record -> `{ subject, orgSlug,
25
+ orgRole?, actor }`, or a typed refusal (not live, not a person's, no
26
+ organisation, organisation inactive or unmapped). Only the token's own
27
+ organisation is ever looked up.
28
+ - `resolvePersonActingName` / `checkPersonCallShape`: the own-name rule. The
29
+ acting identity comes from the principal; another user's name and an agent's
30
+ name without that agent's credential are refused.
31
+ - `resolvePersonTenantAccess`: strict organisation equality; unstamped refused.
32
+ - `resolveWriterRole` / `requireWriterRole`: writer-role assertion, fail closed.
33
+ - `PERSON_ACTOR_PREFIX`, `personActorName`, `isPersonActorName`,
34
+ `personTokenRecordSchema`, `PersonRefusalError` and the refusal types.
35
+ - New subpath export `./person-principal`.
36
+
37
+ ## [0.8.0]
38
+
39
+ **Additive — authorize a PERSON and bind an organisation (0.8.0).**
9
40
  A connector's `/authorize` that auto-approves with no user authentication, and
10
41
  derives the code's user from a client-registration profile, makes every client
11
42
  an anonymous app. This release is the flow that replaces it: Clerk session,
package/README.md CHANGED
@@ -186,6 +186,15 @@ Visibility is granted two ways: the row's `createdBy` appears in the caller's
186
186
  `namespaceReadPrefixes`. Prefixes match on a path boundary, so a caller allowed
187
187
  `team/finance` does not thereby see `team/finance-archive`.
188
188
 
189
+ A third way is opt-in per call. All three functions take an optional last
190
+ argument, `grantFields` (type `GrantFieldDeclaration`, a list of property
191
+ names). It names the row properties that carry per-row grants, such as a
192
+ mission's `agents`. A row is then also visible when one of those properties
193
+ names an identity in the caller's `fromAllowList`. The property can hold a
194
+ single string or an array of strings; a value of any other shape is skipped,
195
+ not matched. The list is data supplied by your call site; the package
196
+ hardcodes no field names. Omitting it, or passing `[]`, adds nothing.
197
+
189
198
  **Token validation**
190
199
 
191
200
  - `validateMasterBearer(header, secret)` — compares an `Authorization: Bearer`
@@ -213,6 +222,8 @@ want to catch by type, and `BearerPayload`, the type returned by
213
222
  - `ValidateMasterBearerResult` — the return type of `validateMasterBearer`.
214
223
  - `ScopeFilterable` — the minimal row shape (`{ createdBy?, namespace? }`)
215
224
  accepted by `passesScopeFilter` / `scopeFilterList` / `scopeFilterGet`.
225
+ - `GrantFieldDeclaration` — `readonly string[]`, the optional `grantFields`
226
+ argument of those three functions.
216
227
  - `SessionIdentity` — the human-path shape `requireTenantId` accepts under
217
228
  `{ kind: "session", identity }` (see "Both caller paths, one contract").
218
229
  - `TenantSource` — the discriminated union `requireTenantId` accepts:
@@ -370,6 +381,75 @@ Limits, stated: `redirect_uri` matching is exact (no loopback port variation);
370
381
  the package signs no access token and no id token and issues no refresh token (the consumer mints the token
371
382
  from the claims); it fetches no key set and calls no Clerk API itself.
372
383
 
384
+ ## Wiring person sign-in in an MCP server
385
+
386
+ A person signed in through an OAuth connector reaches your MCP server holding a
387
+ bearer you minted for them (see "Authorize a person and bind an organisation").
388
+ `@vantageos/cloud-identity/person-principal` turns that bearer's token record
389
+ into the person's principal and answers the three questions every write needs.
390
+ It is pure: you do the lookups, it decides. A server that serves people (a PDF
391
+ inspector, a registry, a CRM) wires it the same way:
392
+
393
+ ```ts
394
+ import {
395
+ resolvePersonPrincipal,
396
+ resolvePersonActingName,
397
+ resolvePersonTenantAccess,
398
+ resolveWriterRole,
399
+ type PersonPrincipal,
400
+ type PersonRefusal,
401
+ } from "@vantageos/cloud-identity/person-principal";
402
+
403
+ // 1. Who is calling: the token row you looked up by the bearer's digest.
404
+ const who = await resolvePersonPrincipal(
405
+ { principal: row.principal, subject: row.userId, orgSlug: row.orgSlug,
406
+ orgRole: row.orgRole, revokedAt: row.revokedAt, expiresAt: row.expiresAt },
407
+ { now: Date.now(), lookupOrganisation: (slug) => orgTable.get(slug) },
408
+ "my-tool",
409
+ );
410
+ if (!who.ok) return deny(who.refusal);
411
+
412
+ // 2. Under what name: the token's, never the argument's.
413
+ const name = resolvePersonActingName({
414
+ principal: who.principal, claimedName: args.createdBy,
415
+ agentCredential: presentedAgent, door: "my-tool",
416
+ });
417
+ if (!name.ok) return deny(name.refusal);
418
+
419
+ // 3. Which rows, and may they write.
420
+ const inOrg = resolvePersonTenantAccess({
421
+ principal: who.principal, rowOrgId: record.orgId, door: "my-tool" });
422
+ const mayWrite = resolveWriterRole({
423
+ role: who.principal.orgRole, writerRoles: writerRolesOfOrg, door: "my-tool",
424
+ orgSlug: who.principal.orgSlug });
425
+ ```
426
+
427
+ - `resolvePersonPrincipal(token, deps, door)` refuses a token that is expired,
428
+ revoked, not a person's, bound to no organisation, or bound to an inactive or
429
+ unmapped one. It looks up only the token's own organisation.
430
+ - `resolvePersonActingName` returns `{ ok: true, actingAs: "person", actor }`
431
+ for no name or the person's own (`user:<subject>`), and refuses another
432
+ user's name (`PERSON_ACTS_AS_ITSELF`) and an agent's name without that
433
+ agent's own credential (`AGENT_CREDENTIAL_REQUIRED`, or
434
+ `AGENT_IDENTITY_MISMATCH` when the credential belongs to another agent).
435
+ `checkPersonCallShape` is the same rule for a backend door that receives a
436
+ person's call already stripped of its name. `PERSON_ACTOR_PREFIX`,
437
+ `personActorName` and `isPersonActorName` spell and reserve the actor name:
438
+ refuse it when someone registers an agent.
439
+ - `resolvePersonTenantAccess` is a strict equality on the organisation; an
440
+ unstamped row is refused.
441
+ - `resolveWriterRole` / `requireWriterRole` check the verified role against a
442
+ writer allowlist you hold as data. A missing role, a missing list and an
443
+ empty list all refuse. `requireWriterRole` throws `PersonRefusalError`.
444
+ - Every refusal is a `PersonRefusal` (`PersonRefusalCode`, `PersonRefusalReason`,
445
+ `door`, `detail`); you choose the transport shape. `personTokenRecordSchema`,
446
+ `PersonTokenRecord`, `PersonPrincipal`, `OrganisationState`,
447
+ `PersonPrincipalDeps`, `PersonPrincipalResult`, `PersonActingNameResult` and
448
+ `PersonAccessResult` are the input and result types.
449
+
450
+ The module verifies no credential. The token record must come from your own
451
+ lookup of the bearer the person presented, never from a request body.
452
+
373
453
  ## What this package does not do
374
454
 
375
455
  Worth reading before you rely on it.
package/dist/index.d.ts CHANGED
@@ -27,4 +27,6 @@ export { buildDiscoveryDocument, buildUserInfo } from "./oidc.js";
27
27
  export type { DiscoveryConfig, DiscoveryDocument, DiscoveryResult, ClerkUserLike, UserInfo, UserInfoResult, } from "./oidc.js";
28
28
  export { oauthErrorFor, pkceChallengeFromVerifier } from "./authorize-shared.js";
29
29
  export type { AuthorizeRefusal, AuthorizeRefusalReason, } from "./authorize-shared.js";
30
+ export { PERSON_ACTOR_PREFIX, PersonRefusalError, personActorName, isPersonActorName, personTokenRecordSchema, resolvePersonPrincipal, resolvePersonActingName, checkPersonCallShape, resolvePersonTenantAccess, resolveWriterRole, requireWriterRole, } from "./person-principal.js";
31
+ export type { PersonRefusal, PersonRefusalCode, PersonRefusalReason, PersonTokenRecord, PersonPrincipal, OrganisationState, PersonPrincipalDeps, PersonPrincipalResult, PersonActingNameResult, PersonAccessResult, } from "./person-principal.js";
30
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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;AAI/B,OAAO,EACL,cAAc,EACd,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChE,YAAY,EACV,qBAAqB,EACrB,aAAa,EACb,YAAY,EACZ,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,YAAY,EACV,QAAQ,EACR,SAAS,EACT,0BAA0B,EAC1B,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAClE,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,QAAQ,EACR,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACjF,YAAY,EACV,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -25,4 +25,8 @@ export { exchangeAuthorizationCode } from "./token-exchange.js";
25
25
  export { verifyClerkSessionToken } from "./clerk-session.js";
26
26
  export { buildDiscoveryDocument, buildUserInfo } from "./oidc.js";
27
27
  export { oauthErrorFor, pkceChallengeFromVerifier } from "./authorize-shared.js";
28
+ // 0.9.0 — The person principal: a verified sign-in (person + organisation) as
29
+ // pure data, the own-name actor rule, the tenant compare and the writer-role
30
+ // assertion. Every consumer wires a person the same way.
31
+ export { PERSON_ACTOR_PREFIX, PersonRefusalError, personActorName, isPersonActorName, personTokenRecordSchema, resolvePersonPrincipal, resolvePersonActingName, checkPersonCallShape, resolvePersonTenantAccess, resolveWriterRole, requireWriterRole, } from "./person-principal.js";
28
32
  //# 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,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;AAY/B,+EAA+E;AAC/E,mEAAmE;AACnE,OAAO,EACL,cAAc,EACd,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAa7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAOhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAQ7D,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AASlE,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,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;AAY/B,+EAA+E;AAC/E,mEAAmE;AACnE,OAAO,EACL,cAAc,EACd,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAa7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAOhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAQ7D,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AASlE,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAMjF,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,oBAAoB,EACpB,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * @vantageos/cloud-identity 0.9.0 — the person principal.
3
+ *
4
+ * A PERSON signed in through an OAuth connector (Claude, ChatGPT, any MCP
5
+ * client) is not an agent. This module is the single, framework-agnostic
6
+ * answer to four questions a consumer must not re-implement:
7
+ *
8
+ * 1. Who is the person, and in which organisation?
9
+ * `resolvePersonPrincipal` — from the record of an access token that the
10
+ * consumer already looked up by its digest. Never from an argument.
11
+ * 2. Under what name does the person act?
12
+ * `resolvePersonActingName` / `checkPersonCallShape` — its OWN name,
13
+ * "user:<subject>", taken from the principal. An agent name needs that
14
+ * agent's own verified credential; another user's name is refused.
15
+ * 3. Is a row in the person's organisation?
16
+ * `resolvePersonTenantAccess` — a strict equality on the organisation.
17
+ * 4. May the person write?
18
+ * `resolveWriterRole` / `requireWriterRole` — the verified role must be on
19
+ * the writer allowlist the consumer holds as data. Fail closed.
20
+ *
21
+ * Pure functions over plain inputs: no Clerk SDK, no database, no network, no
22
+ * master or bypass flag anywhere in the input. The consumer performs the
23
+ * lookups (the token row, the organisation row, the writer list) and passes the
24
+ * results in; this module only decides. Every refusal is a typed value carrying
25
+ * its own code, reason and door, so a caller can branch on content, and an
26
+ * absence (no organisation, no role, no list) is a refusal, never a default.
27
+ *
28
+ * @security This module verifies NOTHING about the credential itself. The
29
+ * token record must come from a lookup of the bearer the person presented, made
30
+ * by the consumer's trusted transport layer. Passing a client-supplied object
31
+ * makes that object the source of subject, organisation and role.
32
+ */
33
+ import { z } from "zod";
34
+ export type PersonRefusalCode = "RBAC_DENIED" | "PERSON_ACTS_AS_ITSELF" | "AGENT_CREDENTIAL_REQUIRED" | "AGENT_IDENTITY_MISMATCH";
35
+ export type PersonRefusalReason = "person-token-not-live" | "not-a-person-token" | "person-token-no-org" | "org-not-active" | "person-acts-as-itself" | "agent-proof-on-person-path" | "agent-credential-required" | "agent-identity-mismatch" | "other-organisation" | "role-not-writer";
36
+ /** The typed refusal. A non-empty object carrying its own code and door. */
37
+ export type PersonRefusal = {
38
+ code: PersonRefusalCode;
39
+ reason: PersonRefusalReason;
40
+ /** The consumer's own name for the entry point that refused. */
41
+ door: string;
42
+ /** One sentence, safe to show to the caller. Never a credential. */
43
+ detail: string;
44
+ /** The acting name the caller typed, when the refusal is about a name. */
45
+ claimed?: string;
46
+ /** The person's own actor name, when the refusal is about a name. */
47
+ self?: string;
48
+ /** The verified role, when the refusal is about a role (null: none). */
49
+ role?: string | null;
50
+ /** The organisation the decision was taken in, when it applies. */
51
+ orgSlug?: string | null;
52
+ };
53
+ /** Thrown by the throwing forms. Carries the typed refusal untouched. */
54
+ export declare class PersonRefusalError extends Error {
55
+ readonly refusal: PersonRefusal;
56
+ constructor(refusal: PersonRefusal);
57
+ }
58
+ /** How a person is recorded as "who did it": "user:<verified subject>". */
59
+ export declare const PERSON_ACTOR_PREFIX = "user:";
60
+ /** The actor name of a verified subject. */
61
+ export declare function personActorName(subject: string): string;
62
+ /**
63
+ * True when `name` is spelled like a person's actor name. Such a name is
64
+ * RESERVED: a consumer refuses it at agent registration, so a stored actor
65
+ * "user:..." can only have been written from a verified subject.
66
+ */
67
+ export declare function isPersonActorName(name: string): boolean;
68
+ /**
69
+ * The record of a presented access token, in the package's vocabulary. The
70
+ * consumer maps its own row onto this shape (`subject` is the verified Clerk
71
+ * user id, `orgSlug` the organisation the token is bound to).
72
+ */
73
+ export declare const personTokenRecordSchema: z.ZodObject<{
74
+ principal: z.ZodOptional<z.ZodString>;
75
+ subject: z.ZodString;
76
+ orgSlug: z.ZodOptional<z.ZodString>;
77
+ orgRole: z.ZodOptional<z.ZodString>;
78
+ revokedAt: z.ZodOptional<z.ZodNumber>;
79
+ expiresAt: z.ZodNumber;
80
+ }, z.core.$strip>;
81
+ export type PersonTokenRecord = z.input<typeof personTokenRecordSchema>;
82
+ /** A verified person: who, in which organisation, with which role. */
83
+ export type PersonPrincipal = {
84
+ subject: string;
85
+ orgSlug: string;
86
+ /** Present only when the token carries a verified role. Never defaulted. */
87
+ orgRole?: string;
88
+ /** "user:<subject>" — the name this person acts under. */
89
+ actor: string;
90
+ };
91
+ /** The state of an organisation as the consumer's own mapping holds it. */
92
+ export type OrganisationState = {
93
+ isActive: boolean;
94
+ };
95
+ export type PersonPrincipalDeps<O extends OrganisationState> = {
96
+ /** Epoch milliseconds, supplied by the caller so the decision is pure. */
97
+ now: number;
98
+ /** Resolves the organisation row for a slug; null when unmapped. */
99
+ lookupOrganisation: (orgSlug: string) => Promise<O | null>;
100
+ };
101
+ export type PersonPrincipalResult<O extends OrganisationState> = {
102
+ ok: true;
103
+ principal: PersonPrincipal;
104
+ organisation: O;
105
+ } | {
106
+ ok: false;
107
+ refusal: PersonRefusal;
108
+ };
109
+ /**
110
+ * Resolves the person behind a presented token, or refuses. Checks, in order:
111
+ * the token is live (present, unrevoked, unexpired); it acts for a PERSON;
112
+ * it is bound to an organisation; that organisation exists and is active.
113
+ * The only organisation ever looked up is the token's own.
114
+ */
115
+ export declare function resolvePersonPrincipal<O extends OrganisationState>(token: PersonTokenRecord | null | undefined, deps: PersonPrincipalDeps<O>, door: string): Promise<PersonPrincipalResult<O>>;
116
+ export type PersonActingNameResult = {
117
+ ok: true;
118
+ actingAs: "person";
119
+ actor: string;
120
+ } | {
121
+ ok: true;
122
+ actingAs: "agent";
123
+ actor: string;
124
+ } | {
125
+ ok: false;
126
+ refusal: PersonRefusal;
127
+ };
128
+ /**
129
+ * Decides under which name a person acts. The acting identity comes from the
130
+ * principal, never from the typed name:
131
+ *
132
+ * - no name, or the person's own name -> acts as itself;
133
+ * - another user's name -> PERSON_ACTS_AS_ITSELF;
134
+ * - an agent's name -> only with that agent's own verified
135
+ * credential (`agentCredential`,
136
+ * resolved by the consumer): none ->
137
+ * AGENT_CREDENTIAL_REQUIRED, a
138
+ * different agent -> AGENT_IDENTITY_MISMATCH.
139
+ */
140
+ export declare function resolvePersonActingName(input: {
141
+ /** Only the actor name is read: the rule needs no organisation. */
142
+ principal: Pick<PersonPrincipal, "actor">;
143
+ claimedName?: string | null;
144
+ /** The agent the presented credential resolved to, when one was presented. */
145
+ agentCredential?: {
146
+ agentName: string;
147
+ } | null;
148
+ door: string;
149
+ }): PersonActingNameResult;
150
+ /**
151
+ * The shape of a call that reaches a backend door ON a person's behalf: it
152
+ * carries no acting name (the transport already stripped the person's own) and
153
+ * no agent proof (a person that presented an agent credential acts as that
154
+ * agent, on the agent path). Returns the refusal, or null when the shape is
155
+ * right.
156
+ */
157
+ export declare function checkPersonCallShape(input: {
158
+ door: string;
159
+ actingName?: string;
160
+ agentProof?: boolean;
161
+ }): PersonRefusal | null;
162
+ export type PersonAccessResult = {
163
+ ok: true;
164
+ } | {
165
+ ok: false;
166
+ refusal: PersonRefusal;
167
+ };
168
+ /**
169
+ * A row is reachable only when its organisation stamp EQUALS the person's
170
+ * organisation. An absent or empty stamp never equals a resolved organisation,
171
+ * so an unstamped row is refused.
172
+ */
173
+ export declare function resolvePersonTenantAccess(input: {
174
+ principal: PersonPrincipal;
175
+ rowOrgId: string | null | undefined;
176
+ door: string;
177
+ }): PersonAccessResult;
178
+ type WriterRoleInput = {
179
+ /** The VERIFIED role claim of the token. Absent or null is refused. */
180
+ role: string | null | undefined;
181
+ /** The writer allowlist the consumer holds as data. Absent or empty refuses all. */
182
+ writerRoles: readonly string[] | null | undefined;
183
+ door: string;
184
+ orgSlug?: string | null;
185
+ };
186
+ /**
187
+ * Non-throwing writer-role assertion. The role must appear EXACTLY in the
188
+ * allowlist; no hierarchy, no inheritance. An absent role, an absent list and
189
+ * an empty list all refuse: a missing list never means "all roles".
190
+ */
191
+ export declare function resolveWriterRole(input: WriterRoleInput): PersonAccessResult;
192
+ /** Throwing form of {@link resolveWriterRole}; throws `PersonRefusalError`. */
193
+ export declare function requireWriterRole(input: WriterRoleInput): void;
194
+ export {};
195
+ //# sourceMappingURL=person-principal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"person-principal.d.ts","sourceRoot":"","sources":["../src/person-principal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @vantageos/cloud-identity 0.9.0 — the person principal.
3
+ *
4
+ * A PERSON signed in through an OAuth connector (Claude, ChatGPT, any MCP
5
+ * client) is not an agent. This module is the single, framework-agnostic
6
+ * answer to four questions a consumer must not re-implement:
7
+ *
8
+ * 1. Who is the person, and in which organisation?
9
+ * `resolvePersonPrincipal` — from the record of an access token that the
10
+ * consumer already looked up by its digest. Never from an argument.
11
+ * 2. Under what name does the person act?
12
+ * `resolvePersonActingName` / `checkPersonCallShape` — its OWN name,
13
+ * "user:<subject>", taken from the principal. An agent name needs that
14
+ * agent's own verified credential; another user's name is refused.
15
+ * 3. Is a row in the person's organisation?
16
+ * `resolvePersonTenantAccess` — a strict equality on the organisation.
17
+ * 4. May the person write?
18
+ * `resolveWriterRole` / `requireWriterRole` — the verified role must be on
19
+ * the writer allowlist the consumer holds as data. Fail closed.
20
+ *
21
+ * Pure functions over plain inputs: no Clerk SDK, no database, no network, no
22
+ * master or bypass flag anywhere in the input. The consumer performs the
23
+ * lookups (the token row, the organisation row, the writer list) and passes the
24
+ * results in; this module only decides. Every refusal is a typed value carrying
25
+ * its own code, reason and door, so a caller can branch on content, and an
26
+ * absence (no organisation, no role, no list) is a refusal, never a default.
27
+ *
28
+ * @security This module verifies NOTHING about the credential itself. The
29
+ * token record must come from a lookup of the bearer the person presented, made
30
+ * by the consumer's trusted transport layer. Passing a client-supplied object
31
+ * makes that object the source of subject, organisation and role.
32
+ */
33
+ import { z } from "zod";
34
+ /** Thrown by the throwing forms. Carries the typed refusal untouched. */
35
+ export class PersonRefusalError extends Error {
36
+ refusal;
37
+ constructor(refusal) {
38
+ super(`${refusal.code}: ${refusal.detail} (${refusal.reason}, ${refusal.door})`);
39
+ this.name = "PersonRefusalError";
40
+ this.refusal = refusal;
41
+ }
42
+ }
43
+ function refuse(code, reason, door, detail, extra = {}) {
44
+ return { code, reason, door, detail, ...extra };
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Actor name
48
+ // ---------------------------------------------------------------------------
49
+ /** How a person is recorded as "who did it": "user:<verified subject>". */
50
+ export const PERSON_ACTOR_PREFIX = "user:";
51
+ /** NFC, lowercase, trim: the comparison form of an acting name. */
52
+ function normalizeName(name) {
53
+ return name.normalize("NFC").toLowerCase().trim();
54
+ }
55
+ /** The actor name of a verified subject. */
56
+ export function personActorName(subject) {
57
+ return `${PERSON_ACTOR_PREFIX}${subject}`;
58
+ }
59
+ /**
60
+ * True when `name` is spelled like a person's actor name. Such a name is
61
+ * RESERVED: a consumer refuses it at agent registration, so a stored actor
62
+ * "user:..." can only have been written from a verified subject.
63
+ */
64
+ export function isPersonActorName(name) {
65
+ return normalizeName(name).startsWith(PERSON_ACTOR_PREFIX);
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // 1. The principal
69
+ // ---------------------------------------------------------------------------
70
+ /**
71
+ * The record of a presented access token, in the package's vocabulary. The
72
+ * consumer maps its own row onto this shape (`subject` is the verified Clerk
73
+ * user id, `orgSlug` the organisation the token is bound to).
74
+ */
75
+ export const personTokenRecordSchema = z.object({
76
+ principal: z.string().optional(),
77
+ subject: z.string(),
78
+ orgSlug: z.string().optional(),
79
+ orgRole: z.string().optional(),
80
+ revokedAt: z.number().optional(),
81
+ expiresAt: z.number(),
82
+ });
83
+ /**
84
+ * Resolves the person behind a presented token, or refuses. Checks, in order:
85
+ * the token is live (present, unrevoked, unexpired); it acts for a PERSON;
86
+ * it is bound to an organisation; that organisation exists and is active.
87
+ * The only organisation ever looked up is the token's own.
88
+ */
89
+ export async function resolvePersonPrincipal(token, deps, door) {
90
+ const deny = (reason, detail) => ({
91
+ ok: false,
92
+ refusal: refuse("RBAC_DENIED", reason, door, detail),
93
+ });
94
+ const parsed = personTokenRecordSchema.safeParse(token);
95
+ if (!parsed.success ||
96
+ parsed.data.revokedAt !== undefined ||
97
+ parsed.data.expiresAt < deps.now) {
98
+ return deny("person-token-not-live", "verifiedPerson names no live access token");
99
+ }
100
+ const row = parsed.data;
101
+ if (row.principal !== "person" || row.subject === "") {
102
+ return deny("not-a-person-token", "verifiedPerson names a token that does not act for a person");
103
+ }
104
+ const orgSlug = row.orgSlug;
105
+ if (orgSlug === undefined || orgSlug === "") {
106
+ return deny("person-token-no-org", "the person's token is bound to no organisation");
107
+ }
108
+ const organisation = await deps.lookupOrganisation(orgSlug);
109
+ if (organisation === null || !organisation.isActive) {
110
+ return deny("org-not-active", `Org "${orgSlug}" not in client_org_mapping or inactive`);
111
+ }
112
+ return {
113
+ ok: true,
114
+ organisation,
115
+ principal: {
116
+ subject: row.subject,
117
+ orgSlug,
118
+ ...(row.orgRole !== undefined ? { orgRole: row.orgRole } : {}),
119
+ actor: personActorName(row.subject),
120
+ },
121
+ };
122
+ }
123
+ /**
124
+ * Decides under which name a person acts. The acting identity comes from the
125
+ * principal, never from the typed name:
126
+ *
127
+ * - no name, or the person's own name -> acts as itself;
128
+ * - another user's name -> PERSON_ACTS_AS_ITSELF;
129
+ * - an agent's name -> only with that agent's own verified
130
+ * credential (`agentCredential`,
131
+ * resolved by the consumer): none ->
132
+ * AGENT_CREDENTIAL_REQUIRED, a
133
+ * different agent -> AGENT_IDENTITY_MISMATCH.
134
+ */
135
+ export function resolvePersonActingName(input) {
136
+ const { principal, door } = input;
137
+ const claimed = input.claimedName;
138
+ if (claimed === undefined || claimed === null) {
139
+ return { ok: true, actingAs: "person", actor: principal.actor };
140
+ }
141
+ const name = normalizeName(claimed);
142
+ if (name === normalizeName(principal.actor)) {
143
+ return { ok: true, actingAs: "person", actor: principal.actor };
144
+ }
145
+ if (name.startsWith(PERSON_ACTOR_PREFIX)) {
146
+ return {
147
+ ok: false,
148
+ refusal: refuse("PERSON_ACTS_AS_ITSELF", "person-acts-as-itself", door, `a person acts only in its own name; this call names "${claimed}"`, { claimed, self: principal.actor }),
149
+ };
150
+ }
151
+ const credential = input.agentCredential;
152
+ if (credential === undefined || credential === null) {
153
+ return {
154
+ ok: false,
155
+ refusal: refuse("AGENT_CREDENTIAL_REQUIRED", "agent-credential-required", door, `this call names "${claimed}" but carried no credential of that agent`, { claimed, self: principal.actor }),
156
+ };
157
+ }
158
+ if (normalizeName(credential.agentName) !== name) {
159
+ return {
160
+ ok: false,
161
+ refusal: refuse("AGENT_IDENTITY_MISMATCH", "agent-identity-mismatch", door, `this call names "${claimed}" but the presented credential belongs to another agent`, { claimed, self: principal.actor }),
162
+ };
163
+ }
164
+ return { ok: true, actingAs: "agent", actor: credential.agentName };
165
+ }
166
+ /**
167
+ * The shape of a call that reaches a backend door ON a person's behalf: it
168
+ * carries no acting name (the transport already stripped the person's own) and
169
+ * no agent proof (a person that presented an agent credential acts as that
170
+ * agent, on the agent path). Returns the refusal, or null when the shape is
171
+ * right.
172
+ */
173
+ export function checkPersonCallShape(input) {
174
+ if (input.agentProof === true) {
175
+ return refuse("RBAC_DENIED", "agent-proof-on-person-path", input.door, "a person's call carries no agent credential and no verified actor");
176
+ }
177
+ if (input.actingName !== undefined) {
178
+ return refuse("PERSON_ACTS_AS_ITSELF", "person-acts-as-itself", input.door, `a person acts only in its own name; this call also names "${input.actingName}"`, { claimed: input.actingName });
179
+ }
180
+ return null;
181
+ }
182
+ /**
183
+ * A row is reachable only when its organisation stamp EQUALS the person's
184
+ * organisation. An absent or empty stamp never equals a resolved organisation,
185
+ * so an unstamped row is refused.
186
+ */
187
+ export function resolvePersonTenantAccess(input) {
188
+ const { principal, rowOrgId, door } = input;
189
+ if (typeof rowOrgId === "string" && rowOrgId !== "" && rowOrgId === principal.orgSlug) {
190
+ return { ok: true };
191
+ }
192
+ return {
193
+ ok: false,
194
+ refusal: refuse("RBAC_DENIED", "other-organisation", door, "the row is outside the person's organisation (tenant boundary)", { orgSlug: principal.orgSlug }),
195
+ };
196
+ }
197
+ /**
198
+ * Non-throwing writer-role assertion. The role must appear EXACTLY in the
199
+ * allowlist; no hierarchy, no inheritance. An absent role, an absent list and
200
+ * an empty list all refuse: a missing list never means "all roles".
201
+ */
202
+ export function resolveWriterRole(input) {
203
+ const role = input.role ?? null;
204
+ const writerRoles = input.writerRoles ?? [];
205
+ if (role !== null && writerRoles.includes(role))
206
+ return { ok: true };
207
+ return {
208
+ ok: false,
209
+ refusal: refuse("RBAC_DENIED", "role-not-writer", input.door, "member role is not a writer role", { role, orgSlug: input.orgSlug ?? null }),
210
+ };
211
+ }
212
+ /** Throwing form of {@link resolveWriterRole}; throws `PersonRefusalError`. */
213
+ export function requireWriterRole(input) {
214
+ const r = resolveWriterRole(input);
215
+ if (!r.ok)
216
+ throw new PersonRefusalError(r.refusal);
217
+ }
218
+ //# sourceMappingURL=person-principal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"person-principal.js","sourceRoot":"","sources":["../src/person-principal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA0CxB,yEAAyE;AACzE,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,OAAO,CAAgB;IAChC,YAAY,OAAsB;QAChC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;QACjF,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,SAAS,MAAM,CACb,IAAuB,EACvB,MAA2B,EAC3B,IAAY,EACZ,MAAc,EACd,QAA+E,EAAE;IAEjF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;AAClD,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,2EAA2E;AAC3E,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAE3C,mEAAmE;AACnE,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,GAAG,mBAAmB,GAAG,OAAO,EAAE,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC;AAC7D,CAAC;AAED,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC,CAAC;AA2BH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,KAA2C,EAC3C,IAA4B,EAC5B,IAAY;IAEZ,MAAM,IAAI,GAAG,CACX,MAA2B,EAC3B,MAAc,EACyB,EAAE,CAAC,CAAC;QAC3C,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC;KACrD,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,uBAAuB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACxD,IACE,CAAC,MAAM,CAAC,OAAO;QACf,MAAM,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS;QACnC,MAAM,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAChC,CAAC;QACD,OAAO,IAAI,CAAC,uBAAuB,EAAE,2CAA2C,CAAC,CAAC;IACpF,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC;IACxB,IAAI,GAAG,CAAC,SAAS,KAAK,QAAQ,IAAI,GAAG,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC;QACrD,OAAO,IAAI,CACT,oBAAoB,EACpB,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5B,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC,qBAAqB,EAAE,gDAAgD,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,YAAY,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC;QACpD,OAAO,IAAI,CACT,gBAAgB,EAChB,QAAQ,OAAO,yCAAyC,CACzD,CAAC;IACJ,CAAC;IACD,OAAO;QACL,EAAE,EAAE,IAAI;QACR,YAAY;QACZ,SAAS,EAAE;YACT,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,OAAO;YACP,GAAG,CAAC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9D,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC;SACpC;KACF,CAAC;AACJ,CAAC;AAWD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAOvC;IACC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IAClC,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,CAAC;IAClC,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QAC9C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC;IAClE,CAAC;IACD,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,IAAI,KAAK,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC;IAClE,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACzC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,MAAM,CACb,uBAAuB,EACvB,uBAAuB,EACvB,IAAI,EACJ,wDAAwD,OAAO,GAAG,EAClE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,CACnC;SACF,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,eAAe,CAAC;IACzC,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACpD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,MAAM,CACb,2BAA2B,EAC3B,2BAA2B,EAC3B,IAAI,EACJ,oBAAoB,OAAO,2CAA2C,EACtE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,CACnC;SACF,CAAC;IACJ,CAAC;IACD,IAAI,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;QACjD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,MAAM,CACb,yBAAyB,EACzB,yBAAyB,EACzB,IAAI,EACJ,oBAAoB,OAAO,yDAAyD,EACpF,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,CACnC;SACF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAIpC;IACC,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,MAAM,CACX,aAAa,EACb,4BAA4B,EAC5B,KAAK,CAAC,IAAI,EACV,mEAAmE,CACpE,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,MAAM,CACX,uBAAuB,EACvB,uBAAuB,EACvB,KAAK,CAAC,IAAI,EACV,6DAA6D,KAAK,CAAC,UAAU,GAAG,EAChF,EAAE,OAAO,EAAE,KAAK,CAAC,UAAU,EAAE,CAC9B,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAIzC;IACC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IAC5C,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,IAAI,QAAQ,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC;QACtF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IACD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,MAAM,CACb,aAAa,EACb,oBAAoB,EACpB,IAAI,EACJ,gEAAgE,EAChE,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,CAC/B;KACF,CAAC;AACJ,CAAC;AAeD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAsB;IACtD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC;IAChC,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC;IAC5C,IAAI,IAAI,KAAK,IAAI,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACrE,OAAO;QACL,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,MAAM,CACb,aAAa,EACb,iBAAiB,EACjB,KAAK,CAAC,IAAI,EACV,kCAAkC,EAClC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,CACzC;KACF,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAAC,KAAsB;IACtD,MAAM,CAAC,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,MAAM,IAAI,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AACrD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/cloud-identity",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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",
@@ -53,6 +53,10 @@
53
53
  "./oidc": {
54
54
  "types": "./dist/oidc.d.ts",
55
55
  "import": "./dist/oidc.js"
56
+ },
57
+ "./person-principal": {
58
+ "types": "./dist/person-principal.d.ts",
59
+ "import": "./dist/person-principal.js"
56
60
  }
57
61
  },
58
62
  "files": [