@agimon-ai/po-identity 0.0.0-stage → 0.0.2-alpha.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/LICENSE ADDED
@@ -0,0 +1,149 @@
1
+ License text copyright © 2017 MariaDB Corporation Ab, All Rights Reserved.
2
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
3
+
4
+ Parameters
5
+
6
+ Licensor: Agimon Pty Ltd
7
+ Licensed Work: Po, the software in the Po source repository and the
8
+ packages released from it, in each version released by
9
+ the Licensor.
10
+ The Licensed Work is (c) 2026 Agimon Pty Ltd.
11
+ Material identified as third-party in the "Third-party
12
+ components" section at the end of this file or in a
13
+ NOTICE file, and any package or directory whose own
14
+ license file grants a different license, is not part of
15
+ the Licensed Work and remains under its own license.
16
+ Additional Use Grant: You may make production use of the Licensed Work if
17
+ your use is Eligible Use. "Eligible Use" is production use that falls
18
+ within at least one of the following:
19
+
20
+ (a) Personal Use. Use by an individual for their own personal purposes,
21
+ and not on behalf of, or for the benefit of, an employer, client, or
22
+ other Organization. Use by an individual in the course of employment
23
+ or engagement by an Organization is use by that Organization.
24
+
25
+ (b) Small Organization Use. Use by or on behalf of an Organization whose
26
+ Annual Revenue is less than USD 1,000,000.
27
+
28
+ (c) Non-profit, Education and Research Use. Use, regardless of Annual
29
+ Revenue, by (i) an Organization registered or recognized as a
30
+ non-profit or charitable organization under the laws of its
31
+ jurisdiction, for its non-profit purposes; (ii) an accredited school,
32
+ college, or university, for teaching, learning, or academic research;
33
+ or (iii) a publicly funded research institution, for its research.
34
+
35
+ (d) Service Provider Use. Use by a consultant, contractor, agency, or other
36
+ service provider, regardless of its own Annual Revenue, solely to
37
+ build, deploy, host, operate, or support the Licensed Work for a client
38
+ whose own use would be Eligible Use under (a), (b), or (c). A service
39
+ provider's use for its own purposes is Eligible Use only if the service
40
+ provider itself qualifies under (b) or (c).
41
+
42
+
43
+ Hosted services. Offering the Licensed Work to third parties as a hosted
44
+ or managed service whose primary value is the Licensed Work is Eligible
45
+ Use only if each such third party's own use would be Eligible Use.
46
+
47
+ Definitions. "Organization" means any company, partnership, sole
48
+ proprietorship, trust, association, government body, or other entity, and
49
+ includes an individual acting in a business or professional capacity.
50
+ "Affiliate" means any entity that directly or indirectly controls, is
51
+ controlled by, or is under common control with an Organization, where
52
+ "control" means ownership of more than 50% of the voting interests of an
53
+ entity or the power to direct its management and policies by contract or
54
+ otherwise. "Annual Revenue" means the total gross revenue of an
55
+ Organization and all of its Affiliates combined, from all sources and
56
+ whether or not related to the Licensed Work, for its most recently
57
+ completed fiscal year, or, if it has not completed a fiscal year, for the
58
+ twelve months before the use (or since its formation, if shorter),
59
+ converted into US dollars at the average exchange rate for that period.
60
+ Proceeds of equity or debt financing are not revenue.
61
+
62
+ Threshold. If an Organization stops qualifying under (b), its existing
63
+ production use remains Eligible Use for 90 days after it stops
64
+ qualifying, after which it must obtain a commercial license or stop
65
+ production use. Use that was Eligible Use when it occurred never requires
66
+ a commercial license after the fact.
67
+ Change Date: Four years from the date a version of the Licensed Work
68
+ is first publicly released by the Licensor.
69
+ Change License: Apache License, Version 2.0
70
+
71
+ For information about alternative licensing arrangements for the Licensed
72
+ Work, please visit: https://agimon.ai/licensing
73
+
74
+ Notice
75
+
76
+ The Business Source License (this document, or the "License") is not an Open
77
+ Source license. However, the Licensed Work will eventually be made available
78
+ under an Open Source License, as stated in this License.
79
+
80
+ -----------------------------------------------------------------------------
81
+
82
+ Business Source License 1.1
83
+
84
+ Terms
85
+
86
+ The Licensor hereby grants you the right to copy, modify, create derivative
87
+ works, redistribute, and make non-production use of the Licensed Work. The
88
+ Licensor may make an Additional Use Grant, above, permitting limited
89
+ production use.
90
+
91
+ Effective on the Change Date, or the fourth anniversary of the first publicly
92
+ available distribution of a specific version of the Licensed Work under this
93
+ License, whichever comes first, the Licensor hereby grants you rights under
94
+ the terms of the Change License, and the rights granted in the paragraph
95
+ above terminate.
96
+
97
+ If your use of the Licensed Work does not comply with the requirements
98
+ currently in effect as described in this License, you must purchase a
99
+ commercial license from the Licensor, its affiliated entities, or authorized
100
+ resellers, or you must refrain from using the Licensed Work.
101
+
102
+ All copies of the original and modified Licensed Work, and derivative works
103
+ of the Licensed Work, are subject to this License. This License applies
104
+ separately for each version of the Licensed Work and the Change Date may vary
105
+ for each version of the Licensed Work released by Licensor.
106
+
107
+ You must conspicuously display this License on each original or modified copy
108
+ of the Licensed Work. If you receive the Licensed Work in original or
109
+ modified form from a third party, the terms and conditions set forth in this
110
+ License apply to your use of that work.
111
+
112
+ Any use of the Licensed Work in violation of this License will automatically
113
+ terminate your rights under this License for the current and all other
114
+ versions of the Licensed Work.
115
+
116
+ This License does not grant you any right in any trademark or logo of
117
+ Licensor or its affiliates (provided that you may use a trademark or logo of
118
+ Licensor as expressly required by this License).
119
+
120
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
121
+ AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
122
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
123
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
124
+ TITLE.
125
+
126
+ MariaDB hereby grants you permission to use this License’s text to license
127
+ your works, and to refer to it using the trademark “Business Source License”,
128
+ as long as you comply with the Covenants of Licensor below.
129
+
130
+ Covenants of Licensor
131
+
132
+ In consideration of the right to use this License’s text and the “Business
133
+ Source License” name and trademark, Licensor covenants to MariaDB, and to all
134
+ other recipients of the licensed work to be provided by Licensor:
135
+
136
+ 1. To specify as the Change License the GPL Version 2.0 or any later version,
137
+ or a license that is compatible with GPL Version 2.0 or a later version,
138
+ where “compatible” means that software provided under the Change License can
139
+ be included in a program with software provided under GPL Version 2.0 or a
140
+ later version. Licensor may specify additional Change Licenses without
141
+ limitation.
142
+
143
+ 2. To either: (a) specify an additional grant of rights to use that does not
144
+ impose any additional restriction on the right granted in this License, as
145
+ the Additional Use Grant; or (b) insert the text “None”.
146
+
147
+ 3. To specify a Change Date.
148
+
149
+ 4. Not to modify this License in any other way.
package/README.md CHANGED
@@ -1,3 +1,268 @@
1
- # Temporary Holding Version
1
+ # Po identity
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Selectable global identity home for Node and SQLite-backed Durable Objects. Better Auth and its
4
+ Expo, SSO and passkey integrations are pinned to 1.7.12. Pairing-only hosts do not compose this package.
5
+ No organization is created at signup. `personalOrg(subject)` provisions the personal organization,
6
+ owner membership and unique subject marker in one transaction.
7
+
8
+ ## Composition
9
+
10
+ Node's global plugin consumes only the server environment explicitly admitted by the CLI, once at
11
+ composition. It injects `po/sqlite` and `po/fs`. It does not use a session's `po/config`: that would let
12
+ one session configure an issuer shared by every session. This was the coordinator-approved phase-1
13
+ configuration decision. Configuration changes require restart. A future global config service can
14
+ provide hot reload. The Worker uses the same `identityConfiguration(env)` function.
15
+
16
+ Required environment keys, with no defaults:
17
+
18
+ - `PO_IDENTITY_ISSUER`, `PO_IDENTITY_JWT_KEY_WRAP` (at least 32 characters).
19
+ - `PO_UNSENDBOX_ENDPOINT`, `PO_UNSENDBOX_ORGANIZATION_ID`, `PO_UNSENDBOX_API_KEY`, `PO_UNSENDBOX_FROM`.
20
+
21
+ Optional OAuth configuration: `PO_APPLE_CLIENT_ID`, `PO_APPLE_CLIENT_SECRET`, `PO_APPLE_BUNDLE_ID`,
22
+ `PO_GOOGLE_CLIENT_ID`, `PO_GOOGLE_CLIENT_SECRET`, `PO_MICROSOFT_CLIENT_ID`,
23
+ `PO_MICROSOFT_CLIENT_SECRET`, `PO_GITHUB_CLIENT_ID`, `PO_GITHUB_CLIENT_SECRET`.
24
+ An enabled provider requires its secret. Apple's client secret is a server-held Apple client-secret
25
+ JWT, never an app credential. Operators must renew it before expiry. Native code exchange uses the
26
+ bundle ID when configured, rather than silently exchanging a native code as a web Services ID.
27
+
28
+ `PO_IDENTITY_WEB_RP_ID` enables web account passkeys against the configured issuer origin. Native
29
+ account passkeys remain disabled. Expo trusts `po://`; native scheme/capability changes belong
30
+ to phase 2. SSO includes OIDC and SAML, with no unsolicited-login override.
31
+
32
+ Node stores `identity.sqlite` under the admitted Po home, respecting `PO_HOME`. The Worker binds
33
+ `PO_IDENTITY` to one `PoIdentityObject` per configured issuer. Secrets are operator bindings, not
34
+ checked-in values. Hosts must serve the issuer through trusted HTTPS in production.
35
+
36
+ ## Issuer endpoints
37
+
38
+ All paths are under `/api/auth/`. Node mounts them only when the global identity API is composed.
39
+ The Worker branches to the identity object before its legacy attach-token gate. Other routes keep
40
+ their existing authorization. Better Auth's session-only `/token` and built-in deletion endpoints
41
+ are disabled: they must not bypass device binding or the deletion outbox.
42
+
43
+ - Better Auth: email OTP, magic link, configured social providers, SSO and optional web passkeys.
44
+ - `GET jwks`: public signing keys. Private keys and OAuth tokens are wrapped at rest.
45
+ - `POST personal-org`: authenticated lazy provisioning.
46
+ - `POST devices/enroll`: authenticated first-device TOFU with proof of the new private key.
47
+ A durable enrollment marker forbids another TOFU after enrollment or revocation.
48
+ Returns a 256-bit recovery code once; only a salted PBKDF2-SHA-256 hash (600,000 iterations)
49
+ is stored. Optional `assurance: "hardware" | "software"` is signed and stored as self-reported.
50
+ Software enrollment is allowed; App Attest and Play Integrity are future attestation upgrades.
51
+ Later devices need a current trusted-device C10 `approval` or an owned hub's `hubEnrollment`.
52
+ - `POST identity-token`: session plus fresh enrolled-device proof, current person and membership.
53
+ C1 claims bind the device JWK thumbprint and network audience, with a 600-second lifetime.
54
+ - `POST delegated-token`: fresh calling-host proof plus enrolled-device-signed C5 consent. It needs
55
+ no person session. The composition must supply `callingHostKey(networkId, hostId)` from current
56
+ roster authority. Without that port it fails closed, not to a caller-advertised key. C2 binds
57
+ the host key, host ID and grant ID; expiry is the smaller of one hour and grant expiry.
58
+ - `POST device-key`: cookie-free lookup with `IdentityDeviceKeyRequest` from `apiContracts`.
59
+ Send `{proof}` signed with `po/host-request/v1` over `{issuer, hostId, keyId, issuedAt,
60
+ expiresAt, nonce}` by the hub key previously admitted through `hubs/claim`. Lifetime is at
61
+ most 60 seconds, with the same conservative 30-second skew deadline. Nonces (16 to 128
62
+ base64url characters) are durably single-use per host, including failed key lookups.
63
+ Returns `{keyId, issuer, subject, publicKey, revoked, revocationEpoch, trust, organizationId}`
64
+ from the registry,
65
+ never from caller-advertised keys. Only the claiming person's keys or keys of current
66
+ members of a hub's claimed organization where its claimant is still a member are visible. Unknown, out-of-scope, malformed,
67
+ stale, replayed, unenrolled and revoked-host requests all return the same no-store 404.
68
+ Migration 3 preserves legacy owner-only hub scope; fresh valid claims record org scopes,
69
+ including multiple organizations per hub.
70
+ The registry's `identity_hub.revoked` flag refuses lookup and further enrollment/claims.
71
+ - `GET devices`: same-person device list, public account keys, trust, self-reported assurance,
72
+ current person `revocationEpoch`, the durable `hasEnrolled` marker and any pending recovery.
73
+ - `POST devices/approval/request`: signed-in new-key `approval-request` proof creates an opaque
74
+ 16-character, five-minute code. `GET devices/approval?code=...` is same-person lookup/polling;
75
+ `POST devices/approval` admits the trusted device's signed C10 envelope. The new device then
76
+ calls `devices/enroll` with a fresh new-key proof and that approval. Final admission is durable
77
+ and single-use. Both devices must be signed in; no email address enters the approval.
78
+ - `POST hubs/claim`: current enrolled-device C10 claim plus the nested QR host attestation records
79
+ the person's hub key. The hub must independently enforce its own local pin; the issuer never
80
+ authorizes on its behalf. Advertised unregistered hub keys cannot approve enrollment.
81
+ - `POST devices/recovery/code`: `{proof, recoveryCode}` with action `recover-code` enrolls at once
82
+ and consumes the code. `POST devices/recovery/start`: `{proof}` with action `recover-start`
83
+ persists a 72-hour due time. It durably queues a start email with a cancel link and one notice
84
+ per trusted device through `notifyTrustedDevice`. Missing or failed delivery ports leave jobs
85
+ pending. The email token is hashed in the pending row and wrapped in the delivery outbox.
86
+ - `POST devices/recovery/cancel`: `{cancelToken}` from email, or `{proof}` signed by a trusted
87
+ same-person device or the requesting new device. Action `cancel-recovery` binds
88
+ `targetDeviceId` to the recovery ID. The email GET only displays confirmation, avoiding
89
+ mutation by link scanners. Completion durably queues a second email.
90
+ - `POST devices/recovery/admin-approve`: current enrolled org owner/admin proof, action
91
+ `admin-approve`, `org` the limiting organization and `targetDeviceId` the pending recovery ID.
92
+ The signed-in pending device and admin must both belong to that organization. The resulting
93
+ device can mint only for that org and cannot approve unrestricted devices or claim hubs.
94
+ - `POST devices/revoke`: enrolled-device proof and a same-person target. Revocation immediately
95
+ stops new mints, including C2 grants signed by that device.
96
+ - `GET revocations?after=<sequence>`: public cursor feed, no addresses or credentials, no cache.
97
+ Enrolled hosts can fetch it without a person cookie or host proof.
98
+ - `POST delete-account`: refuses a sole company owner with other members, deletes person-owned
99
+ identity and device records, emits revocation, and durably queues personal-org/cloud-workspace
100
+ purge and Apple revocation before removing credentials.
101
+
102
+ Proofs use `identityProofFields(issuer, subject, action, proof)` with the tag
103
+ `po/identity-mint/v1` and P-256 SHA-256 P1363 base64url signatures. Actions include `enroll`, `mint`, `delegate`, `revoke`, `approval-request`, `recover-code`,
104
+ `recover-start`, `cancel-recovery` and `admin-approve`. The helper binds timestamps, nonce, org, device ID, optional enrollment
105
+ key, optional revocation target and the exact serialized grant. Proof times are milliseconds;
106
+ JWT dates are seconds. Proof lifetime is at most 60 seconds. Future issue time is at most 30
107
+ seconds; acceptance ends at expiry minus 30 seconds, inclusive. When supplied, assurance appends `["assurance", value]` to the signed fields; omitted assurance
108
+ keeps phase-1 signed bytes unchanged. Nonces are admitted durably in
109
+ transactions before tokens are released. Signing is followed by a current-authority recheck.
110
+ A destination still needs phase-3 JWT validation, C3/C4 proofs, durable nonce admission and local
111
+ authorization. Issuance is not workspace permission.
112
+
113
+ Native Apple sign-in sends `authorizationCode` beside `idToken: { token, nonce }` to
114
+ `sign-in/social`. Missing code or nonce is refused. The issuer verifies Apple's JWKS, issuer,
115
+ audience, time and nonce, exchanges the one-use code, verifies the exchanged subject and stores
116
+ an encrypted issuer/subject/provider-bound refresh credential before returning a session.
117
+ Deletion retains that ciphertext through Apple outages and retries via the outbox. No refresh
118
+ token is returned to the app. Provider response bodies and addresses are never logged.
119
+
120
+ ## Persistence and limits
121
+
122
+ The SQLite adapter is the Better Auth spike adapter moved into runtime source, with literal predicates,
123
+ one-row versus many-row mutations, transaction-local executors, counts and versioned migration
124
+ admission. Initial schema creation is tied to the pinned plugin versions. Plugin/schema upgrades
125
+ need explicit new migrations; this is not a general Better Auth migration engine or a completed
126
+ upstream adapter conformance certification. The Node/workerd rollback and concurrency tests
127
+ now import the real package adapter.
128
+
129
+ Node drains the identity outbox on start and periodically. Recovery uses the injected
130
+ `scheduleRecovery` port and a Node scheduler. The identity DO's single alarm multiplexes recovery
131
+ due times and outbox retries; both deadlines are restored from SQLite after reconstruction.
132
+ `runDueRecovery` uses the injected clock and atomically completes only due, uncancelled requests.
133
+
134
+ Every completed recovery increments the person's epoch atomically with device insertion and a
135
+ feed row carrying `epoch`. The recovered device stores that enrollment epoch. SQLite refuses
136
+ non-increasing epoch updates. Other enrolled devices stay trusted, but C5 `revocationEpoch` must
137
+ exactly equal the current epoch to mint C2. Missing grant epochs mean zero, deliberately voiding
138
+ all legacy grants after the first recovery. C1/C2 JWTs carry `revocation_epoch`.
139
+
140
+ Phase 3 still needs destination JWT/feed checks. A pre-recovery identity token can remain valid
141
+ until its own expiry, at most ten minutes, at a destination that has not fetched the feed.
142
+ Delegated tokens retain their existing at-most-one-hour expiry. Propagation is not bounded here.
143
+
144
+ Hub policy is exported as `createHubDeviceTrust` from core's remote-access store. Owner claims,
145
+ account pins and replay admission persist in `hub-trust.json`. Approval requires a device already
146
+ pinned here and verified current issuer device authority. Recovered keys need a fresh QR pin in
147
+ the current recovery epoch. Issuer enrollment alone is never owner-level authority. Phase 3 must
148
+ compose this gate into federation control routes; it is intentionally not wired into legacy
149
+ layer-2 native registration, whose host-scoped keys differ from the account device key.
150
+
151
+ ### Phase-4 provisioning API
152
+
153
+ `ensurePersonalOrg(subject, 'cloud-workspace')` atomically creates the personal org, owner membership,
154
+ unique `(issuer, subject)` marker, first workspace allocation and materialization outbox item.
155
+ Concurrent calls and retries return the same `{orgId, workspaceId}`. Slug collisions retry inside
156
+ that transaction (up to 16 attempts). Existing subject-only markers migrate without replacing orgs.
157
+ `PersonalOrgTrigger` reserves `own-hub-link` for phase 5; it is currently refused.
158
+
159
+ The authenticated `POST personal-org` accepts `{trigger: "cloud-workspace"}` and returns
160
+ `{organizationId, orgId, workspaceId}` for bootstrap before device enrollment and token minting.
161
+ Legacy `{}` and `personalOrg(subject)` remain org-only compatibility calls. Signup still creates no org.
162
+
163
+ The trusted internal API also exposes `allocateWorkspace(subject, orgId)` (a new allocation per call),
164
+ `listOrgWorkspaces(subject, orgId)` and `isWorkspaceAllocatedToOrg(subject, orgId, workspaceId)`.
165
+ All require a current account and org membership. The last method returns false for unallocated ids.
166
+ The Worker must authenticate the subject before calling these methods; they are not bearer endpoints.
167
+ `orgRole` and `federationAuthority` expose live membership and device/epoch authority to its verifier.
168
+ The latter fails closed on issuer, membership, device revocation and restricted-org mismatches.
169
+
170
+ `listProvisioningOutbox()` returns typed `{id, kind: "materialize-workspace", payload: {orgId, workspaceId}}`
171
+ items only. `ackProvisioningOutbox(id)` is idempotent and cannot ack recovery, purge or Apple jobs.
172
+ Alternatively the host supplies `materializeWorkspace(payload)` and calls `drainOutbox()` (also used
173
+ by `runDueRecovery`). Missing consumers and failed deliveries stay pending. Materialization is an
174
+ at-least-once, separate cross-object step: its consumer must be idempotent. Hosts restore retry
175
+ alarms from persisted pending jobs on boot and schedule after allocation. Identity commits never
176
+ claim cross-object atomicity. Deletion cancels personal-org allocation/materialization records and
177
+ queues the existing purge job. Host-side purge and in-flight materialization need coordinated
178
+ idempotent handling; production alarm delivery and full cloud deletion remain Worker verification.
179
+
180
+ Types are exported from the package root and `apiContracts`. No dependency changes are required.
181
+ Registry migration 4 adds provisioning tables and repairs the monotonic epoch update trigger when
182
+ opening older v2 databases that did not have it. SQLite upgrade tests preserve v2 devices, hubs and
183
+ pending outbox data. The purge consumer is a host port. Until wired, purge jobs remain pending durably.
184
+ No purge SLA, roster cadence or emergency JWKS-cache bound is invented here.
185
+ Calling-host roster authority and destination authorization remain separate phases. Apple exchange/revocation tests use fake HTTP providers. Real providers, native
186
+ Apple return, deployment, production quotas and provider key rotation remain unverified.
187
+
188
+ ## Phase-1 local evidence
189
+
190
+ Final checks in the multihost worktree:
191
+
192
+ ```sh
193
+ NX_DAEMON=false pnpm nx run-many -p @agimon-ai/po-identity @agimon-ai/po-cloudflare @agimon-ai/po-core @agimon-ai/po-web-security @agimon-ai/po -t lint,typecheck,build --parallel=1 --output-style=static
194
+ NX_DAEMON=false pnpm nx run @agimon-ai/po-cloudflare:test --maxWorkers=1 --output-style=static
195
+ NX_DAEMON=false pnpm nx run-many -p @agimon-ai/po-identity @agimon-ai/po-core @agimon-ai/po-web-security @agimon-ai/po -t test --parallel=1 --output-style=static
196
+ pnpm --filter @agimon-ai/po-cloudflare exec wrangler deploy --dry-run --outdir .wrangler/identity-bundle
197
+ ```
198
+
199
+ All listed Nx targets passed. Full test counts: identity 11, Cloudflare 64, core 1,981, security 126,
200
+ CLI 962 passed with one existing skipped test. A parallel run hit the already documented isolate
201
+ memory-test duration bound; the isolated rerun and full single-worker Cloudflare suite passed.
202
+ No isolate code, assertions or time bounds were changed. Worktree-local vibe-lint preflight,
203
+ including `po-edge-portable`, and `git diff --check` passed.
204
+
205
+ Wrangler 4.150.0 reported total upload **13,553.00 KiB raw, 3,129.00 KiB gzip**. The main emitted
206
+ `worker.js` was **13,375,137 raw bytes, 2,969,373 gzip bytes**; total upload also includes WASM.
207
+ This is the composed Po Worker, including identity and SAML, not the smaller Better Auth and SAML spikes.
208
+ It exceeds a 3 MiB compressed free-worker ceiling. Paid-plan limits, deployment, CPU/startup
209
+ budgets and actual production-provider behavior were not verified. Dry-run artifacts remain ignored.
210
+
211
+ ## Phase-2 verification boundary
212
+
213
+ Focused protocol tests cover signed approval chains, forgery/expiry/replay refusal, owned-hub
214
+ attestation admission, one-use recovery codes, software assurance, org-only admin recovery,
215
+ monotonic epochs, stale/invalid grant epochs, email and device cancellation, exact 72-hour
216
+ completion and restart. A shared fixture runs the real KDF and signature/envelope helpers on
217
+ both Node SQLite and workerd DO SQLite, with only delivery, scheduling and time injected.
218
+ The hub-store tests cover account-association round trips, durable claims and approval chains,
219
+ and recovered-device owner denial until QR. A diagnostic spy checks no recovery address is logged.
220
+
221
+ Real email deliverability, the native push adapter, production DO alarm delivery across eviction,
222
+ production issuer-to-hub authority synchronization and end-to-end native QR attestation remain
223
+ unverified. No dependency or lockfile changes are needed by this protocol implementation.
224
+
225
+ ## Network roster issuer (phase 5)
226
+
227
+ - `GET /api/auth/roster-metadata` publishes `{issuer, rosterPublicKey}`. The key is base64url DER
228
+ P-256 SPKI, not PEM. Claim-time discovery must fetch it from the already trusted issuer and pin
229
+ it locally, never trust a key in a roster response. The dedicated private roster key is persisted
230
+ encrypted with `jwtKeyWrap`. No route exposes it. Rotation currently requires every hub to re-pin;
231
+ the upgrade path is signed key-transition records.
232
+ - `POST /api/networks/own-hub-link` returns `{networkId}` and lazily creates one personal org per
233
+ person in the existing transaction. It does not allocate a cloud workspace.
234
+ - `GET /api/networks/:networkId/roster` returns an issuer-signed C6 envelope. Current member identity
235
+ tokens (ES256, issuer and network audience, live membership, device and recovery epoch) or existing
236
+ signed-in sessions are accepted. Host polling may instead send `x-po-roster-proof`: a JSON
237
+ `po/host-request/v1` envelope signed by the current host key, with `{networkId, hostId,
238
+ destination: issuer, method: 'GET', path: exactPathname, nonce, issuedAt, expiresAt}`. Envelope freshness
239
+ and durable single-use nonces apply; removed or evicted hosts cannot poll.
240
+ - `POST /api/networks/:networkId/hosts` accepts `{entry}` from the entry owner's current membership;
241
+ `POST /api/networks/:networkId/changes` accepts `{change}` from an owner or admin. A change names
242
+ the next epoch. Before any epoch or eviction write, the issuer verifies host entries against the
243
+ submitting owner's enrolled, non-revoked device, and eviction/voter changes against a current
244
+ org owner's enrolled, non-revoked device. Durable records verify as of issuance, with at most
245
+ 30 seconds of future skew. Term changes require a current voter host's own key and verify at
246
+ the current time. These enrollment checks do not replace `applyRoster` with independently pinned keys.
247
+ Host entries, changes and all Better Auth membership writes advance the epoch transactionally.
248
+ Membership removal permanently marks that person's host IDs unavailable.
249
+ - Composition APIs on `createIdentityHome()` are `networkRoster(subject, networkId)`,
250
+ `registerRosterHost(subject, networkId, entry)`, `registerRosterChange(subject, networkId, change)`,
251
+ `rosterIssuerPublicKey()`, `hostKey(networkId, hostId)`, `rosterPins(subject, networkId)`, and
252
+ `registerCloudRosterHost(subject, networkId, {hostId, publicKey, endpoints?})`. The cloud helper
253
+ signs with the issuer and uses reserved device key ID `issuer-roster`; identical calls preserve
254
+ the original owner and epoch. `rosterPins` supplies `{hostId, issuer, subject, keyId, publicKey}`
255
+ records for Po Cloud only. Its trust ceiling is issuer-enrolled devices, not local hub pins.
256
+
257
+ Snapshots retain all evictions, the latest voter change per live host, and the current epoch's term
258
+ changes. The issuer orders these records and may withhold changes. A new joiner skips an eviction
259
+ it cannot verify only if the target host is absent from both its local state and the snapshot.
260
+ It then lacks that tombstone, so a malicious issuer could later re-include an old device-signed host
261
+ entry. Independent hub pins remain mandatory. Compaction requires a signed checkpoint. Roster authorization does not replace C9 quorum commitment, and this issuer
262
+ does not claim Byzantine safety or availability in small networks.
263
+
264
+ Durable device host entries and owner voter/eviction decisions verify as of issuance, including on
265
+ catch-up after their declared expiry. The fresh issuer snapshot supplies freshness. Term diagnostics
266
+ still verify at the current time. Local pin revocation or rotation removes the old key's authority,
267
+ so its old records stop verifying. A compromised still-pinned owner key can backdate records until
268
+ it is unpinned; the issuer cannot correct a host's local trust decision.
@@ -0,0 +1,23 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ exports.__toESM = __toESM;