@cortexkit/antigravity-auth-core 2.2.1 → 3.0.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.
Files changed (230) hide show
  1. package/README.md +24 -2
  2. package/dist/abort-forwarding.d.ts +3 -0
  3. package/dist/abort-forwarding.d.ts.map +1 -0
  4. package/dist/abort-forwarding.js +19 -0
  5. package/dist/abort-forwarding.js.map +1 -0
  6. package/dist/account-identity.d.ts +52 -0
  7. package/dist/account-identity.d.ts.map +1 -0
  8. package/dist/account-identity.js +66 -0
  9. package/dist/account-identity.js.map +1 -0
  10. package/dist/account-manager.d.ts +228 -43
  11. package/dist/account-manager.d.ts.map +1 -1
  12. package/dist/account-manager.js +942 -903
  13. package/dist/account-manager.js.map +1 -1
  14. package/dist/account-migration.d.ts +309 -0
  15. package/dist/account-migration.d.ts.map +1 -0
  16. package/dist/account-migration.js +2282 -0
  17. package/dist/account-migration.js.map +1 -0
  18. package/dist/account-repository-codecs.d.ts +82 -0
  19. package/dist/account-repository-codecs.d.ts.map +1 -0
  20. package/dist/account-repository-codecs.js +609 -0
  21. package/dist/account-repository-codecs.js.map +1 -0
  22. package/dist/account-repository-types.d.ts +831 -0
  23. package/dist/account-repository-types.d.ts.map +1 -0
  24. package/dist/account-repository-types.js +137 -0
  25. package/dist/account-repository-types.js.map +1 -0
  26. package/dist/account-repository.d.ts +442 -0
  27. package/dist/account-repository.d.ts.map +1 -0
  28. package/dist/account-repository.js +2253 -0
  29. package/dist/account-repository.js.map +1 -0
  30. package/dist/account-selector.d.ts +470 -0
  31. package/dist/account-selector.d.ts.map +1 -0
  32. package/dist/account-selector.js +1383 -0
  33. package/dist/account-selector.js.map +1 -0
  34. package/dist/account-storage.d.ts.map +1 -1
  35. package/dist/account-storage.js +5 -0
  36. package/dist/account-storage.js.map +1 -1
  37. package/dist/agy-transport.d.ts.map +1 -1
  38. package/dist/agy-transport.js +21 -35
  39. package/dist/agy-transport.js.map +1 -1
  40. package/dist/antigravity-command-menu.d.ts +171 -0
  41. package/dist/antigravity-command-menu.d.ts.map +1 -0
  42. package/dist/antigravity-command-menu.js +523 -0
  43. package/dist/antigravity-command-menu.js.map +1 -0
  44. package/dist/atomic-write.d.ts +18 -1
  45. package/dist/atomic-write.d.ts.map +1 -1
  46. package/dist/atomic-write.js +15 -3
  47. package/dist/atomic-write.js.map +1 -1
  48. package/dist/common-auth-embedded/NOTICE.txt +21 -0
  49. package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
  50. package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
  51. package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
  52. package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
  53. package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
  54. package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
  55. package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
  56. package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
  57. package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
  58. package/dist/common-auth-embedded/auth-menu/index.js +12 -0
  59. package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
  60. package/dist/common-auth-embedded/auth-menu/login.js +83 -0
  61. package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
  62. package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
  63. package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
  64. package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
  65. package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
  66. package/dist/common-auth-embedded/auth-menu/select.js +159 -0
  67. package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
  68. package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
  69. package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
  70. package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
  71. package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
  72. package/dist/common-auth-embedded/claustrum/custody.js +354 -0
  73. package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
  74. package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
  75. package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
  76. package/dist/common-auth-embedded/claustrum/errors.js +8 -0
  77. package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
  78. package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
  79. package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
  80. package/dist/common-auth-embedded/claustrum/index.js +22 -0
  81. package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
  82. package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
  83. package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
  84. package/dist/common-auth-embedded/claustrum/roster.js +536 -0
  85. package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
  86. package/dist/common-auth-embedded/commands/builtins.js +534 -0
  87. package/dist/common-auth-embedded/commands/index.d.ts +10 -0
  88. package/dist/common-auth-embedded/commands/index.js +5 -0
  89. package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
  90. package/dist/common-auth-embedded/commands/menu.js +300 -0
  91. package/dist/common-auth-embedded/commands/model.d.ts +238 -0
  92. package/dist/common-auth-embedded/commands/model.js +16 -0
  93. package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
  94. package/dist/common-auth-embedded/commands/pi.js +206 -0
  95. package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
  96. package/dist/common-auth-embedded/commands/seam.js +301 -0
  97. package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
  98. package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
  99. package/dist/common-auth-embedded/fs/index.d.ts +7 -0
  100. package/dist/common-auth-embedded/fs/index.js +6 -0
  101. package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
  102. package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
  103. package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
  104. package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
  105. package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
  106. package/dist/common-auth-embedded/fs/with-lock.js +48 -0
  107. package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
  108. package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
  109. package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
  110. package/dist/common-auth-embedded/logger/engine.js +246 -0
  111. package/dist/common-auth-embedded/logger/index.d.ts +6 -0
  112. package/dist/common-auth-embedded/logger/index.js +3 -0
  113. package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
  114. package/dist/common-auth-embedded/logger/redact.js +83 -0
  115. package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
  116. package/dist/common-auth-embedded/pi-slot/index.js +2 -0
  117. package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
  118. package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
  119. package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
  120. package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
  121. package/dist/common-auth-embedded/quota/codec.js +12 -0
  122. package/dist/common-auth-embedded/quota/format.d.ts +46 -0
  123. package/dist/common-auth-embedded/quota/format.js +138 -0
  124. package/dist/common-auth-embedded/quota/index.d.ts +10 -0
  125. package/dist/common-auth-embedded/quota/index.js +5 -0
  126. package/dist/common-auth-embedded/quota/map.d.ts +63 -0
  127. package/dist/common-auth-embedded/quota/map.js +105 -0
  128. package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
  129. package/dist/common-auth-embedded/quota/merge.js +162 -0
  130. package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
  131. package/dist/common-auth-embedded/quota/projection.js +134 -0
  132. package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
  133. package/dist/common-auth-embedded/routing/admission.js +142 -0
  134. package/dist/common-auth-embedded/routing/index.d.ts +8 -0
  135. package/dist/common-auth-embedded/routing/index.js +4 -0
  136. package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
  137. package/dist/common-auth-embedded/routing/ordered.js +58 -0
  138. package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
  139. package/dist/common-auth-embedded/routing/pins.js +34 -0
  140. package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
  141. package/dist/common-auth-embedded/routing/sticky.js +339 -0
  142. package/dist/common-auth-embedded/source-output.json +1090 -0
  143. package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
  144. package/dist/common-auth-embedded/store/attribution.js +60 -0
  145. package/dist/common-auth-embedded/store/errors.d.ts +55 -0
  146. package/dist/common-auth-embedded/store/errors.js +38 -0
  147. package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
  148. package/dist/common-auth-embedded/store/hooks.js +34 -0
  149. package/dist/common-auth-embedded/store/identity.d.ts +39 -0
  150. package/dist/common-auth-embedded/store/identity.js +79 -0
  151. package/dist/common-auth-embedded/store/index.d.ts +21 -0
  152. package/dist/common-auth-embedded/store/index.js +7 -0
  153. package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
  154. package/dist/common-auth-embedded/store/mutate.js +500 -0
  155. package/dist/common-auth-embedded/store/pool.d.ts +196 -0
  156. package/dist/common-auth-embedded/store/pool.js +120 -0
  157. package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
  158. package/dist/common-auth-embedded/store/provider-state.js +226 -0
  159. package/dist/common-auth-embedded/store/publication.d.ts +47 -0
  160. package/dist/common-auth-embedded/store/publication.js +304 -0
  161. package/dist/common-auth-embedded/store/pull.d.ts +36 -0
  162. package/dist/common-auth-embedded/store/pull.js +143 -0
  163. package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
  164. package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
  165. package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
  166. package/dist/common-auth-embedded/store/refresh.js +225 -0
  167. package/dist/common-auth-embedded/store/reserved.js +13 -0
  168. package/dist/common-auth-embedded/store/rows.d.ts +313 -0
  169. package/dist/common-auth-embedded/store/rows.js +1258 -0
  170. package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
  171. package/dist/common-auth-embedded/store/runtime.js +68 -0
  172. package/dist/common-auth-embedded/store/schema.d.ts +443 -0
  173. package/dist/common-auth-embedded/store/schema.js +754 -0
  174. package/dist/common-auth-embedded/store/settings.d.ts +63 -0
  175. package/dist/common-auth-embedded/store/settings.js +120 -0
  176. package/dist/common-auth-embedded/store/torn.d.ts +78 -0
  177. package/dist/common-auth-embedded/store/torn.js +294 -0
  178. package/dist/common-auth-embedded/util/any-signal.js +42 -0
  179. package/dist/common-auth-runtime.d.ts +73 -0
  180. package/dist/common-auth-runtime.d.ts.map +1 -0
  181. package/dist/common-auth-runtime.js +65 -0
  182. package/dist/common-auth-runtime.js.map +1 -0
  183. package/dist/credential-redaction.d.ts +13 -0
  184. package/dist/credential-redaction.d.ts.map +1 -0
  185. package/dist/credential-redaction.js +230 -0
  186. package/dist/credential-redaction.js.map +1 -0
  187. package/dist/fetch-timeout.d.ts +4 -2
  188. package/dist/fetch-timeout.d.ts.map +1 -1
  189. package/dist/fetch-timeout.js +121 -14
  190. package/dist/fetch-timeout.js.map +1 -1
  191. package/dist/index.d.ts +16 -1
  192. package/dist/index.d.ts.map +1 -1
  193. package/dist/index.js +15 -0
  194. package/dist/index.js.map +1 -1
  195. package/dist/quota-manager.d.ts.map +1 -1
  196. package/dist/quota-manager.js +35 -10
  197. package/dist/quota-manager.js.map +1 -1
  198. package/dist/quota-presentation.d.ts +10 -0
  199. package/dist/quota-presentation.d.ts.map +1 -0
  200. package/dist/quota-presentation.js +60 -0
  201. package/dist/quota-presentation.js.map +1 -0
  202. package/dist/quota-types.d.ts +2 -0
  203. package/dist/quota-types.d.ts.map +1 -1
  204. package/dist/rotation.d.ts +8 -0
  205. package/dist/rotation.d.ts.map +1 -1
  206. package/dist/rotation.js +24 -0
  207. package/dist/rotation.js.map +1 -1
  208. package/dist/transform/claude.d.ts +7 -3
  209. package/dist/transform/claude.d.ts.map +1 -1
  210. package/dist/transform/claude.js +6 -2
  211. package/dist/transform/claude.js.map +1 -1
  212. package/dist/transform/types.d.ts +1 -1
  213. package/dist/transform/types.d.ts.map +1 -1
  214. package/dist/vault-account-source.d.ts +183 -0
  215. package/dist/vault-account-source.d.ts.map +1 -0
  216. package/dist/vault-account-source.js +296 -0
  217. package/dist/vault-account-source.js.map +1 -0
  218. package/dist/vault-enrollment-authority.d.ts +16 -0
  219. package/dist/vault-enrollment-authority.d.ts.map +1 -0
  220. package/dist/vault-enrollment-authority.js +19 -0
  221. package/dist/vault-enrollment-authority.js.map +1 -0
  222. package/dist/vault-family.d.ts +11 -0
  223. package/dist/vault-family.d.ts.map +1 -0
  224. package/dist/vault-family.js +24 -0
  225. package/dist/vault-family.js.map +1 -0
  226. package/dist/vault-provider-state.d.ts +162 -0
  227. package/dist/vault-provider-state.d.ts.map +1 -0
  228. package/dist/vault-provider-state.js +422 -0
  229. package/dist/vault-provider-state.js.map +1 -0
  230. package/package.json +39 -2
@@ -0,0 +1,175 @@
1
+ import { type ClaustrumClient, type ClaustrumReporterSource, type EnrollmentTokenFile } from '@cortexkit/claustrum-client';
2
+ import { type ClaustrumLogger } from './errors.js';
3
+ export type ClaustrumScopedClient = Pick<ClaustrumClient, 'listScoped' | 'getScoped' | 'reportAuthFailureScoped' | 'close'>;
4
+ /** The two credential types a plugin's family can hold; routing differs by type. */
5
+ export type VaultCredentialType = 'oauth' | 'api_key';
6
+ /**
7
+ * Which vault rows belong to this plugin. `refreshAdapter` classifies an OAuth
8
+ * row by the protocol it speaks; `category` is the grant that authorizes this
9
+ * consumer to read it. Static API keys carry no refresh adapter, so they are
10
+ * admitted only when `apiKeys` is set and only by category.
11
+ */
12
+ export interface ClaustrumFamily {
13
+ refreshAdapter: string;
14
+ category: string;
15
+ apiKeys?: boolean;
16
+ }
17
+ /** One vault credential this consumer may serve. Never carries bearer material. */
18
+ export interface VaultCredential {
19
+ readonly credentialId: string;
20
+ readonly credentialType: VaultCredentialType;
21
+ /**
22
+ * The provider account the credential logs into, when the vault's adapter
23
+ * claims one. Absent means the adapter makes no claim, not a mismatch.
24
+ */
25
+ readonly accountIdentity?: string;
26
+ readonly state: string;
27
+ readonly email?: string;
28
+ readonly orgName?: string;
29
+ }
30
+ /**
31
+ * Why a listed vault record could not be used. A closed set of fixed codes, so
32
+ * a log line or a roster file that carries one never echoes vault data.
33
+ */
34
+ export type SkippedVaultReason = 'empty credential id' | 'duplicate credential id' | 'blank account identity' | 'empty state';
35
+ /**
36
+ * A vault record this consumer could not use. `credentialId` is absent when
37
+ * the record's id itself was unusable, so nothing can say which account the
38
+ * record was.
39
+ */
40
+ export interface SkippedVaultRecord {
41
+ readonly credentialId?: string;
42
+ readonly reason: SkippedVaultReason;
43
+ }
44
+ export interface VaultInventory {
45
+ /**
46
+ * The vault's change cursor: a digest over exactly what this consumer can see
47
+ * (ids, grants, state, identity), never over record versions, so a routine
48
+ * token refresh does not move it. Only equality is meaningful.
49
+ */
50
+ readonly view: string;
51
+ readonly credentials: readonly VaultCredential[];
52
+ readonly skipped: readonly SkippedVaultRecord[];
53
+ }
54
+ export interface ClaustrumScopedIdentity {
55
+ readonly credentialId: string;
56
+ readonly credentialType: VaultCredentialType;
57
+ readonly accountIdentity?: string;
58
+ }
59
+ /**
60
+ * Where a receipt's `accountIdentity` came from: the vault asserted it in the
61
+ * served reply, the plugin's `parseIdentity` read it from the served token, or
62
+ * neither did and it is only the roster's expectation (`none`: no identity at
63
+ * all).
64
+ */
65
+ export type AccountIdentitySource = 'asserted' | 'parsed' | 'expected' | 'none';
66
+ /**
67
+ * A receipt: what the vault served for one physical send. Each attempt gets
68
+ * its own. It records the exact record version served, because a 401 for
69
+ * this send is reported to the vault against that version.
70
+ */
71
+ export interface ClaustrumScopedAttempt {
72
+ readonly credentialId: string;
73
+ readonly credentialType: VaultCredentialType;
74
+ /**
75
+ * The account this receipt is bound to: the vault's assertion, else the
76
+ * plugin's parse of the token, else the roster's expectation. Check
77
+ * `accountIdentitySource` before treating it as proof.
78
+ */
79
+ readonly accountIdentity?: string;
80
+ readonly accountIdentitySource: AccountIdentitySource;
81
+ /** The account the roster row named when this receipt was requested. */
82
+ readonly expectedAccountIdentity?: string;
83
+ /** The credential id the vault itself put in the served reply, if any. */
84
+ readonly assertedCredentialId?: string;
85
+ /**
86
+ * The account the vault itself put in the served reply, if any. Never filled
87
+ * in from the roster or from a token parse.
88
+ */
89
+ readonly assertedAccountIdentity?: string;
90
+ /**
91
+ * The non-secret Google Cloud project id the vault served in get_scoped for
92
+ * this attempt, if any. Never filled in from list_scoped, the roster or a
93
+ * token parse. A project change in the vault takes effect on the next send.
94
+ */
95
+ readonly projectId?: string;
96
+ /**
97
+ * Kept in memory only and hidden from JSON.stringify and object spreads, so
98
+ * logging a receipt never leaks it. Authorize again for every dispatch and retry.
99
+ */
100
+ readonly accessToken: string;
101
+ readonly recordVersion: number;
102
+ readonly expiresAtMs: number | null;
103
+ }
104
+ /** Reads the provider identity a served token executes under, when the plugin can tell. */
105
+ export type IdentityParser = (accessToken: string) => string | undefined;
106
+ /** Only a new version of the same account may replace an in-flight 401. */
107
+ export declare function isScopedCredentialRotation(served: ClaustrumScopedAttempt, current: ClaustrumScopedAttempt | undefined): current is ClaustrumScopedAttempt;
108
+ /** Why a scoped 401 did or did not retry. Never carries credential material. */
109
+ export type ScopedRetryReason = 'rotated' | 'reauthorize-failed' | 'credential-changed' | 'account-changed' | 'version-unchanged';
110
+ /**
111
+ * Decide whether a request that got a 401 should retry with the freshly
112
+ * re-authorized receipt, and log the decision. The vault can refresh a
113
+ * credential between the send and the 401; the log line lets that refresh be
114
+ * matched against the consumer's retry. `site` names the kind of request.
115
+ */
116
+ export declare function decideScopedRetryAfter401(site: string, served: ClaustrumScopedAttempt, current: ClaustrumScopedAttempt | undefined, logger?: ClaustrumLogger): current is ClaustrumScopedAttempt;
117
+ /**
118
+ * How long a served OAuth token must stay valid after the vault hands it out,
119
+ * so it cannot expire while a long request is still using it.
120
+ */
121
+ export declare const SERVING_MARGIN_MS = 300000;
122
+ /**
123
+ * Reads this consumer's vault credentials (list, fetch, 401 report), with the
124
+ * enrollment token as authorization. Used by every host of a plugin. There is
125
+ * deliberately no credential cache and no single-flight of credential reads:
126
+ * each physical send is authorized by the vault, so a revoked enrollment or a
127
+ * changed record takes effect on the next send. The enrollment token is
128
+ * re-read per operation so an operator's reissue on disk is picked up.
129
+ */
130
+ export declare class ClaustrumScopedCustody {
131
+ #private;
132
+ constructor(options: {
133
+ client: ClaustrumScopedClient;
134
+ family: ClaustrumFamily;
135
+ tokenPath?: string;
136
+ readToken?: () => Promise<EnrollmentTokenFile>;
137
+ parseIdentity?: IdentityParser;
138
+ /**
139
+ * Issue a receipt only when the vault's served reply itself names the
140
+ * requested credential id and the roster's (known) account identity. For
141
+ * providers whose tokens are opaque, where nothing else can prove which
142
+ * account a token belongs to. Off by default: then an absent assertion is
143
+ * no claim, and the receipt says where its identity came from.
144
+ */
145
+ requireAssertion?: boolean;
146
+ now?: () => number;
147
+ logger?: ClaustrumLogger;
148
+ });
149
+ /**
150
+ * List this consumer's credentials. A record that cannot be used is skipped
151
+ * and warned about rather than failing the whole list, so one bad record
152
+ * never hides every other account. Records outside the family are not
153
+ * skipped records: they simply are not this consumer's.
154
+ */
155
+ discover(signal?: AbortSignal): Promise<VaultInventory>;
156
+ /**
157
+ * Fetch the credential for one physical send and wrap it in a fresh receipt.
158
+ * The vault's served identity (or, without one, the plugin's parse of the
159
+ * token) must equal the roster's when both are present. Without
160
+ * `requireAssertion`, absence on either side proves nothing and does not
161
+ * refuse; the receipt records what the vault asserted separately from what
162
+ * the roster expected. With it, a reply that does not itself name the
163
+ * credential id and the expected account is refused.
164
+ */
165
+ authorize(identity: ClaustrumScopedIdentity, signal?: AbortSignal): Promise<ClaustrumScopedAttempt>;
166
+ /**
167
+ * Report that a send this consumer actually made was rejected. Only a 401
168
+ * is reported (the vault refuses 429, 402 and 5xx reports), addressed by
169
+ * credential id with the exact record version that send was served, and
170
+ * signed with the enrollment token that authorized it. A receipt this
171
+ * custody did not issue is refused rather than reported.
172
+ */
173
+ reportFailure(attempt: ClaustrumScopedAttempt, status: number, reporterSource: ClaustrumReporterSource): Promise<void>;
174
+ close(): void;
175
+ }
@@ -0,0 +1,354 @@
1
+ import { ClaustrumCredentialError, } from '@cortexkit/claustrum-client';
2
+ import { createLogger } from '../logger/index.js';
3
+ import { readClaustrumEnrollmentToken } from './enrollment.js';
4
+ import { ClaustrumConsumerError } from './errors.js';
5
+ import { CUSTODY_PLACEHOLDER_PREFIX } from './host-slot.js';
6
+ /** Only a new version of the same account may replace an in-flight 401. */
7
+ export function isScopedCredentialRotation(served, current) {
8
+ return (current !== undefined &&
9
+ current.credentialId === served.credentialId &&
10
+ current.accountIdentity === served.accountIdentity &&
11
+ current.recordVersion !== served.recordVersion);
12
+ }
13
+ function scopedRetryReason(served, current) {
14
+ if (current === undefined)
15
+ return 'reauthorize-failed';
16
+ if (current.credentialId !== served.credentialId)
17
+ return 'credential-changed';
18
+ if (current.accountIdentity !== served.accountIdentity)
19
+ return 'account-changed';
20
+ if (current.recordVersion === served.recordVersion)
21
+ return 'version-unchanged';
22
+ return 'rotated';
23
+ }
24
+ const defaultLogger = createLogger('claustrum');
25
+ /**
26
+ * Decide whether a request that got a 401 should retry with the freshly
27
+ * re-authorized receipt, and log the decision. The vault can refresh a
28
+ * credential between the send and the 401; the log line lets that refresh be
29
+ * matched against the consumer's retry. `site` names the kind of request.
30
+ */
31
+ export function decideScopedRetryAfter401(site, served, current, logger = defaultLogger) {
32
+ const retry = isScopedCredentialRotation(served, current);
33
+ logger.debug('scoped 401 re-authorized', {
34
+ site,
35
+ credentialId: served.credentialId,
36
+ servedVersion: served.recordVersion,
37
+ currentVersion: current?.recordVersion ?? null,
38
+ retry,
39
+ reason: scopedRetryReason(served, current),
40
+ });
41
+ return retry;
42
+ }
43
+ /**
44
+ * How long a served OAuth token must stay valid after the vault hands it out,
45
+ * so it cannot expire while a long request is still using it.
46
+ */
47
+ export const SERVING_MARGIN_MS = 300_000;
48
+ function invalidMaterial() {
49
+ return new ClaustrumConsumerError('invalid-material', 'Claustrum returned invalid credential material');
50
+ }
51
+ function accessTokenFromMaterial(material) {
52
+ let access = material.trim();
53
+ if (access.startsWith('{')) {
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(access);
57
+ }
58
+ catch {
59
+ throw invalidMaterial();
60
+ }
61
+ if (!parsed || typeof parsed !== 'object')
62
+ throw invalidMaterial();
63
+ const record = parsed;
64
+ const value = record.access_token ?? record.access;
65
+ access = typeof value === 'string' ? value : '';
66
+ }
67
+ // A header-safe token only: control characters could split the request,
68
+ // and the custody placeholder must never be sent as a credential.
69
+ if (!/^[\x21-\x7e]+$/.test(access) ||
70
+ access.startsWith(CUSTODY_PLACEHOLDER_PREFIX)) {
71
+ throw invalidMaterial();
72
+ }
73
+ return access;
74
+ }
75
+ function credentialTypeOf(row, family) {
76
+ if (!row.operations.includes('read'))
77
+ return undefined;
78
+ if (!row.categories.includes(family.category))
79
+ return undefined;
80
+ if (row.credentialType === 'oauth')
81
+ return row.refreshAdapter === family.refreshAdapter ? 'oauth' : undefined;
82
+ if (row.credentialType === 'api_key' && family.apiKeys)
83
+ return row.refreshAdapter === undefined ? 'api_key' : undefined;
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Reads this consumer's vault credentials (list, fetch, 401 report), with the
88
+ * enrollment token as authorization. Used by every host of a plugin. There is
89
+ * deliberately no credential cache and no single-flight of credential reads:
90
+ * each physical send is authorized by the vault, so a revoked enrollment or a
91
+ * changed record takes effect on the next send. The enrollment token is
92
+ * re-read per operation so an operator's reissue on disk is picked up.
93
+ */
94
+ export class ClaustrumScopedCustody {
95
+ #client;
96
+ #readToken;
97
+ #now;
98
+ #family;
99
+ #parseIdentity;
100
+ #requireAssertion;
101
+ #logger;
102
+ #provenance = new WeakMap();
103
+ #reports = new WeakMap();
104
+ #closed = false;
105
+ constructor(options) {
106
+ const tokenPath = options.tokenPath;
107
+ if (options.readToken) {
108
+ this.#readToken = options.readToken;
109
+ }
110
+ else if (tokenPath) {
111
+ this.#readToken = () => readClaustrumEnrollmentToken(tokenPath);
112
+ }
113
+ else {
114
+ throw new ClaustrumConsumerError('not-enrolled', 'Claustrum enrollment token path is required');
115
+ }
116
+ this.#client = options.client;
117
+ this.#family = options.family;
118
+ this.#parseIdentity = options.parseIdentity;
119
+ this.#requireAssertion = options.requireAssertion ?? false;
120
+ this.#now = options.now ?? Date.now;
121
+ this.#logger = options.logger ?? defaultLogger;
122
+ }
123
+ #check(signal) {
124
+ if (this.#closed)
125
+ throw new ClaustrumConsumerError('closed', 'Claustrum scoped custody is closed');
126
+ signal?.throwIfAborted();
127
+ }
128
+ async #token(signal) {
129
+ this.#check(signal);
130
+ const value = await this.#readToken();
131
+ this.#check(signal);
132
+ if (!/^[0-9a-f]{64}$/.test(value.token) ||
133
+ !Number.isSafeInteger(value.token_generation) ||
134
+ value.token_generation < 1) {
135
+ throw new ClaustrumConsumerError('invalid-token', 'Invalid Claustrum enrollment token');
136
+ }
137
+ return value.token;
138
+ }
139
+ async #call(operation, signal) {
140
+ this.#check(signal);
141
+ let result;
142
+ let removeAbortListener;
143
+ try {
144
+ const pending = operation();
145
+ result = signal
146
+ ? await Promise.race([
147
+ pending,
148
+ new Promise((_resolve, reject) => {
149
+ const abort = () => reject(signal.reason);
150
+ signal.addEventListener('abort', abort, { once: true });
151
+ removeAbortListener = () => signal.removeEventListener('abort', abort);
152
+ if (signal.aborted)
153
+ abort();
154
+ }),
155
+ ])
156
+ : await pending;
157
+ }
158
+ catch (error) {
159
+ this.#check(signal);
160
+ // Keep the vault's own refusals (ClaustrumCredentialError, carrying
161
+ // code, class and action); replace any other error, whose text may echo
162
+ // request params that include the enrollment token.
163
+ if (error instanceof ClaustrumCredentialError)
164
+ throw error;
165
+ throw new ClaustrumConsumerError('unavailable', 'Claustrum scoped operation unavailable');
166
+ }
167
+ finally {
168
+ removeAbortListener?.();
169
+ }
170
+ this.#check(signal);
171
+ return result;
172
+ }
173
+ /**
174
+ * List this consumer's credentials. A record that cannot be used is skipped
175
+ * and warned about rather than failing the whole list, so one bad record
176
+ * never hides every other account. Records outside the family are not
177
+ * skipped records: they simply are not this consumer's.
178
+ */
179
+ async discover(signal) {
180
+ const token = await this.#token(signal);
181
+ const inventory = await this.#call(() => this.#client.listScoped(token), signal);
182
+ const skipped = [];
183
+ const candidates = [];
184
+ const counts = new Map();
185
+ for (const row of inventory.rows) {
186
+ const type = credentialTypeOf(row, this.#family);
187
+ if (!type)
188
+ continue;
189
+ candidates.push({ row, type });
190
+ counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
191
+ }
192
+ const credentials = [];
193
+ for (const { row, type } of candidates) {
194
+ let reason;
195
+ if (!row.id.trim())
196
+ reason = 'empty credential id';
197
+ else if ((counts.get(row.id) ?? 0) > 1)
198
+ reason = 'duplicate credential id';
199
+ else if (row.accountId !== undefined && !row.accountId.trim())
200
+ reason = 'blank account identity';
201
+ else if (!row.state.trim())
202
+ reason = 'empty state';
203
+ if (reason) {
204
+ skipped.push({ ...(row.id.trim() && { credentialId: row.id }), reason });
205
+ continue;
206
+ }
207
+ credentials.push(Object.freeze({
208
+ credentialId: row.id,
209
+ credentialType: type,
210
+ ...(row.accountId !== undefined && {
211
+ accountIdentity: row.accountId,
212
+ }),
213
+ state: row.state,
214
+ ...(row.email !== undefined && { email: row.email }),
215
+ ...(row.orgName !== undefined && { orgName: row.orgName }),
216
+ }));
217
+ }
218
+ for (const record of skipped)
219
+ this.#logger.warn('skipped malformed vault record', record);
220
+ return {
221
+ view: inventory.view,
222
+ credentials: Object.freeze(credentials),
223
+ skipped: Object.freeze(skipped),
224
+ };
225
+ }
226
+ /**
227
+ * Fetch the credential for one physical send and wrap it in a fresh receipt.
228
+ * The vault's served identity (or, without one, the plugin's parse of the
229
+ * token) must equal the roster's when both are present. Without
230
+ * `requireAssertion`, absence on either side proves nothing and does not
231
+ * refuse; the receipt records what the vault asserted separately from what
232
+ * the roster expected. With it, a reply that does not itself name the
233
+ * credential id and the expected account is refused.
234
+ */
235
+ async authorize(identity, signal) {
236
+ if (!identity.credentialId)
237
+ throw new ClaustrumConsumerError('route-unavailable', 'Claustrum dispatch requires a credential id');
238
+ // Capture the caller's fields before yielding so a later mutation cannot move the fence.
239
+ const { credentialId, credentialType, accountIdentity } = identity;
240
+ if (this.#requireAssertion && accountIdentity === undefined)
241
+ throw new ClaustrumConsumerError('identity-unasserted', 'Claustrum dispatch requires a known account identity');
242
+ const token = await this.#token(signal);
243
+ const served = await this.#call(() => this.#client.getScoped({
244
+ credentialId,
245
+ enrollmentToken: token,
246
+ ...(credentialType === 'oauth' && { minTtlMs: SERVING_MARGIN_MS }),
247
+ }), signal);
248
+ if (served.credentialId !== undefined &&
249
+ served.credentialId !== credentialId) {
250
+ throw new ClaustrumConsumerError('identity-changed', 'Claustrum served credential identity changed');
251
+ }
252
+ const expiresAtMs = served.expiresAtMs;
253
+ if (!Number.isSafeInteger(served.recordVersion) ||
254
+ served.recordVersion < 0 ||
255
+ (expiresAtMs === null && credentialType === 'oauth') ||
256
+ (expiresAtMs !== null &&
257
+ (!Number.isFinite(expiresAtMs) ||
258
+ expiresAtMs - this.#now() < SERVING_MARGIN_MS))) {
259
+ throw new ClaustrumConsumerError('insufficient-validity', 'Claustrum served credential has insufficient validity');
260
+ }
261
+ const accessToken = accessTokenFromMaterial(served.material);
262
+ const assertedIdentity = served.accountId?.trim()
263
+ ? served.accountId
264
+ : undefined;
265
+ if (this.#requireAssertion &&
266
+ (served.credentialId === undefined || assertedIdentity === undefined))
267
+ throw new ClaustrumConsumerError('identity-unasserted', 'Claustrum served credential did not assert its identity');
268
+ const parsedIdentity = assertedIdentity === undefined
269
+ ? this.#parseIdentity?.(accessToken)
270
+ : undefined;
271
+ const servedIdentity = assertedIdentity ?? parsedIdentity;
272
+ if (accountIdentity !== undefined &&
273
+ servedIdentity !== undefined &&
274
+ servedIdentity !== accountIdentity) {
275
+ throw new ClaustrumConsumerError('identity-changed', 'Claustrum served credential identity changed');
276
+ }
277
+ const resolvedIdentity = servedIdentity ?? accountIdentity;
278
+ const source = assertedIdentity !== undefined
279
+ ? 'asserted'
280
+ : parsedIdentity !== undefined
281
+ ? 'parsed'
282
+ : accountIdentity !== undefined
283
+ ? 'expected'
284
+ : 'none';
285
+ const attempt = Object.freeze(Object.defineProperty({
286
+ credentialId,
287
+ credentialType,
288
+ ...(resolvedIdentity !== undefined && {
289
+ accountIdentity: resolvedIdentity,
290
+ }),
291
+ accountIdentitySource: source,
292
+ ...(accountIdentity !== undefined && {
293
+ expectedAccountIdentity: accountIdentity,
294
+ }),
295
+ ...(served.credentialId !== undefined && {
296
+ assertedCredentialId: served.credentialId,
297
+ }),
298
+ ...(assertedIdentity !== undefined && {
299
+ assertedAccountIdentity: assertedIdentity,
300
+ }),
301
+ ...(served.projectId !== undefined && {
302
+ projectId: served.projectId,
303
+ }),
304
+ recordVersion: served.recordVersion,
305
+ expiresAtMs,
306
+ }, 'accessToken', { value: accessToken, enumerable: false }));
307
+ this.#provenance.set(attempt, token);
308
+ return attempt;
309
+ }
310
+ /**
311
+ * Report that a send this consumer actually made was rejected. Only a 401
312
+ * is reported (the vault refuses 429, 402 and 5xx reports), addressed by
313
+ * credential id with the exact record version that send was served, and
314
+ * signed with the enrollment token that authorized it. A receipt this
315
+ * custody did not issue is refused rather than reported.
316
+ */
317
+ async reportFailure(attempt, status, reporterSource) {
318
+ if (status !== 401)
319
+ return;
320
+ this.#check();
321
+ const token = this.#provenance.get(attempt);
322
+ if (!token)
323
+ throw new ClaustrumConsumerError('no-receipt', 'Claustrum failure report requires an original dispatch receipt');
324
+ const pending = this.#reports.get(attempt);
325
+ if (pending)
326
+ return pending;
327
+ const report = this.#call(() => this.#client.reportAuthFailureScoped({
328
+ credentialId: attempt.credentialId,
329
+ enrollmentToken: token,
330
+ providerStatus: 401,
331
+ recordVersion: attempt.recordVersion,
332
+ reporterSource,
333
+ }));
334
+ this.#reports.set(attempt, report);
335
+ try {
336
+ await report;
337
+ this.#logger.debug('scoped 401 reported', {
338
+ credentialId: attempt.credentialId,
339
+ recordVersion: attempt.recordVersion,
340
+ reporterSource,
341
+ });
342
+ }
343
+ catch (error) {
344
+ this.#reports.delete(attempt);
345
+ throw error;
346
+ }
347
+ }
348
+ close() {
349
+ if (this.#closed)
350
+ return;
351
+ this.#closed = true;
352
+ this.#client.close();
353
+ }
354
+ }
@@ -0,0 +1,158 @@
1
+ import { type ClaustrumClientOptions as ClaustrumEnrollmentClientOptions, type EnrollmentPollOutcome, type EnrollmentTokenFile, writeEnrollmentTokenFile } from '@cortexkit/claustrum-client';
2
+ /**
3
+ * Codes the vault's closed enrollment-refusal vocabulary marks permanent. The
4
+ * client labels some module error frames transient/retry regardless, so the
5
+ * producer's code takes precedence over the client's action for these.
6
+ */
7
+ export declare const TERMINAL_ENROLLMENT_CODES: ReadonlySet<string>;
8
+ /** Codes that are always worth another ceremony tick, whatever the action says. */
9
+ export declare const RETRYABLE_ENROLLMENT_CODES: ReadonlySet<string>;
10
+ export type EnrollmentDisposition = 'terminal' | 'retry';
11
+ /**
12
+ * Classify an enrollment transport refusal as `(code, disposition)`. The
13
+ * vault's own code wins: a terminal code is terminal even when the client
14
+ * says retry, queue saturation is retryable even when it says gone, and any
15
+ * other code follows the client's action. Errors that are not producer
16
+ * refusals return undefined and are the caller's to rethrow.
17
+ */
18
+ export declare function classifyEnrollmentError(error: unknown): {
19
+ code: string;
20
+ disposition: EnrollmentDisposition;
21
+ } | undefined;
22
+ /**
23
+ * The name a plugin proposes for one host, for example `openai-auth-opencode`.
24
+ * Each host enrolls separately so the operator can revoke one without the other.
25
+ */
26
+ export declare function enrollmentName(plugin: string, host: string): string;
27
+ export interface ClaustrumEnrollmentClient {
28
+ enrollPropose(input: {
29
+ name: string;
30
+ requestSecretHash: string;
31
+ }): Promise<{
32
+ requestId: string;
33
+ }>;
34
+ enrollPoll(input: {
35
+ requestId: string;
36
+ requestSecret: string;
37
+ }): Promise<EnrollmentPollOutcome>;
38
+ }
39
+ export interface ClaustrumEnrollmentConnection extends ClaustrumEnrollmentClient {
40
+ close(): void;
41
+ }
42
+ /**
43
+ * Connect the enrollment ceremony. Setup calls this; the request path never
44
+ * does. `connectionFile` is required because this library reads no
45
+ * environment and knows no host paths.
46
+ */
47
+ export declare function connectClaustrumEnrollmentClient(options: ClaustrumEnrollmentClientOptions & {
48
+ connectionFile: string;
49
+ }): Promise<ClaustrumEnrollmentConnection>;
50
+ export type ClaustrumEnrollmentStatus = {
51
+ state: 'idle';
52
+ } | {
53
+ state: 'pending';
54
+ proposedName: string;
55
+ requestId?: string;
56
+ retryCode?: string;
57
+ } | {
58
+ state: 'approved';
59
+ proposedName: string;
60
+ approvedName?: string;
61
+ tokenGeneration: number;
62
+ } | {
63
+ state: 'denied';
64
+ proposedName: string;
65
+ } | {
66
+ state: 'blocked';
67
+ proposedName: string;
68
+ code: string;
69
+ } | {
70
+ state: 'unavailable';
71
+ proposedName: string;
72
+ code: string;
73
+ } | {
74
+ state: 'busy';
75
+ };
76
+ export interface ClaustrumEnrollmentPaths {
77
+ statePath: string;
78
+ tokenPath: string;
79
+ }
80
+ /** The ceremony state file sits next to the token: `x.json` pairs with `x-state.json`. */
81
+ export declare function getClaustrumEnrollmentPaths(tokenPath: string): ClaustrumEnrollmentPaths;
82
+ /**
83
+ * One host's token and state paths. Every host gets its own pair under the
84
+ * plugin's state directory, so an OpenCode enrollment and a Pi enrollment can
85
+ * be approved and revoked independently. A plugin-resolved override replaces
86
+ * the default token path; a relative override resolves against `cwd`.
87
+ */
88
+ export declare function hostEnrollmentPaths(input: {
89
+ stateDir: string;
90
+ host: string;
91
+ override?: string;
92
+ cwd?: string;
93
+ }): ClaustrumEnrollmentPaths;
94
+ /** Account-file paths are injectable for isolated setup tests; no environment overrides. */
95
+ export interface EnrollmentAncestorOptions {
96
+ passwdPath?: string;
97
+ groupPath?: string;
98
+ }
99
+ /**
100
+ * Protect setup writes from directory replacement. Group write access is safe
101
+ * only when local account records prove that the effective user's group is private.
102
+ */
103
+ export declare function refuseWritableAncestor(parent: string, options?: EnrollmentAncestorOptions): Promise<void>;
104
+ export declare function readClaustrumEnrollmentStatus(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<ClaustrumEnrollmentStatus>;
105
+ /** Resolve the host's account mode from its enrollment files without writing. */
106
+ export declare function enrollmentAuthority(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<'vault' | 'local' | 'unreadable'>;
107
+ /**
108
+ * Read fresh bearer material for one scoped operation; never publish it.
109
+ * Re-reading per operation is what lets an operator reissue a token on disk.
110
+ */
111
+ export declare function readClaustrumEnrollmentToken(tokenPath: string): Promise<EnrollmentTokenFile>;
112
+ /**
113
+ * The enrollment ceremony for one host. Run it from setup only: it proposes,
114
+ * polls and persists, and every step can wait on an operator. The request
115
+ * path reads the resulting token and never calls into this class.
116
+ */
117
+ export declare class ClaustrumEnrollmentManager {
118
+ #private;
119
+ constructor(options: {
120
+ client: ClaustrumEnrollmentClient;
121
+ paths: ClaustrumEnrollmentPaths;
122
+ proposedName: string;
123
+ now?: () => number;
124
+ mintSecret?: () => string;
125
+ writeTokenFile?: typeof writeEnrollmentTokenFile;
126
+ ancestorOptions?: EnrollmentAncestorOptions;
127
+ });
128
+ status(): Promise<ClaustrumEnrollmentStatus>;
129
+ resetTerminal(): Promise<ClaustrumEnrollmentResetResult>;
130
+ reconcile(): Promise<ClaustrumEnrollmentStatus>;
131
+ }
132
+ export type ClaustrumEnrollmentResetResult = 'reset' | 'idle' | 'refused-pending' | 'refused-approved' | 'busy';
133
+ /** Reset local terminal metadata without connecting to the credential daemon. */
134
+ export declare function resetClaustrumEnrollmentState(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<ClaustrumEnrollmentResetResult>;
135
+ export type ClaustrumEnrollmentDisconnectResult = 'disconnected' | 'idle' | 'busy';
136
+ /**
137
+ * Disconnect this host from the vault on disk: remove its enrollment token,
138
+ * then its ceremony state, so `enrollmentAuthority` reports `local` again.
139
+ * This is the disconnect every plugin uses. Removing only the token is not
140
+ * enough: an approved state without a token is still vault mode and refuses,
141
+ * by design. It never contacts the credential daemon, so the enrollment stays
142
+ * approved in the vault until the operator revokes it there; a later Connect
143
+ * starts a new ceremony. A pending request is abandoned the same way.
144
+ *
145
+ * Takes the ceremony lock `reconcile()` and `resetClaustrumEnrollmentState`
146
+ * take, and returns `busy` without deleting anything while another process
147
+ * holds it. State naming another consumer refuses with `wrong-consumer` and
148
+ * both files stay. The token goes first: a crash between the two deletions
149
+ * leaves an approved state without a token, which is still vault mode (it
150
+ * refuses rather than falls back to local accounts), and calling this again
151
+ * finishes the job. Returns `idle` when neither file exists. Nothing else in
152
+ * the directory is touched. The token is removed without being parsed, so a
153
+ * corrupt token does not stand in the way. A malformed state file (bad JSON
154
+ * or wrong shape) is removed too, since it is what makes `enrollmentAuthority`
155
+ * report `unreadable`; a state file that fails the safety checks (symlink,
156
+ * wrong owner, not owner-only) refuses with `unsafe-file` and both files stay.
157
+ */
158
+ export declare function disconnectClaustrumEnrollment(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<ClaustrumEnrollmentDisconnectResult>;