@cortexkit/anthropic-auth-core 1.23.0 → 2.0.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.
Files changed (136) hide show
  1. package/README.md +18 -1
  2. package/dist/accounts.d.ts +4 -163
  3. package/dist/cachekeep.d.ts +9 -0
  4. package/dist/claude-code.d.ts +16 -0
  5. package/dist/claustrum-enrollment.d.ts +2 -0
  6. package/dist/claustrum.d.ts +0 -209
  7. package/dist/commands/account.d.ts +2 -3
  8. package/dist/index.d.ts +45 -4
  9. package/dist/index.js +27569 -38
  10. package/dist/internal-types/LICENSE +21 -0
  11. package/dist/internal-types/commands/builtins.d.ts +71 -0
  12. package/dist/internal-types/commands/index.d.ts +10 -0
  13. package/dist/internal-types/commands/menu.d.ts +61 -0
  14. package/dist/internal-types/commands/model.d.ts +238 -0
  15. package/dist/internal-types/commands/pi.d.ts +23 -0
  16. package/dist/internal-types/commands/seam.d.ts +72 -0
  17. package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
  18. package/dist/internal-types/logger/capture-sink.d.ts +14 -0
  19. package/dist/internal-types/logger/engine.d.ts +98 -0
  20. package/dist/internal-types/logger/index.d.ts +6 -0
  21. package/dist/internal-types/logger/redact.d.ts +11 -0
  22. package/dist/internal-types/pi-slot/index.d.ts +3 -0
  23. package/dist/internal-types/pi-slot/slot.d.ts +51 -0
  24. package/dist/internal-types/quota/codec.d.ts +11 -0
  25. package/dist/internal-types/quota/format.d.ts +46 -0
  26. package/dist/internal-types/quota/index.d.ts +10 -0
  27. package/dist/internal-types/quota/map.d.ts +63 -0
  28. package/dist/internal-types/quota/merge.d.ts +48 -0
  29. package/dist/internal-types/quota/projection.d.ts +64 -0
  30. package/dist/internal-types/store/attribution.d.ts +28 -0
  31. package/dist/internal-types/store/errors.d.ts +55 -0
  32. package/dist/internal-types/store/hooks.d.ts +12 -0
  33. package/dist/internal-types/store/identity.d.ts +39 -0
  34. package/dist/internal-types/store/index.d.ts +21 -0
  35. package/dist/internal-types/store/mutate.d.ts +189 -0
  36. package/dist/internal-types/store/pool.d.ts +197 -0
  37. package/dist/internal-types/store/provider-state.d.ts +131 -0
  38. package/dist/internal-types/store/publication.d.ts +47 -0
  39. package/dist/internal-types/store/pull.d.ts +36 -0
  40. package/dist/internal-types/store/refresh-lock.d.ts +67 -0
  41. package/dist/internal-types/store/refresh.d.ts +72 -0
  42. package/dist/internal-types/store/rows.d.ts +313 -0
  43. package/dist/internal-types/store/runtime.d.ts +36 -0
  44. package/dist/internal-types/store/schema.d.ts +443 -0
  45. package/dist/internal-types/store/settings.d.ts +63 -0
  46. package/dist/internal-types/store/torn.d.ts +78 -0
  47. package/dist/migration-source.d.ts +33 -0
  48. package/dist/models.d.ts +41 -0
  49. package/dist/native-account-runtime.d.ts +121 -0
  50. package/dist/native-account-view.d.ts +126 -0
  51. package/dist/native-command-effects.d.ts +59 -0
  52. package/dist/native-credential-validation.d.ts +42 -0
  53. package/dist/native-custody-activation.d.ts +45 -0
  54. package/dist/native-custody.d.ts +114 -0
  55. package/dist/native-enrollment-authority.d.ts +2 -0
  56. package/dist/native-host-auth-read.d.ts +3 -0
  57. package/dist/native-host-auth-write.d.ts +51 -0
  58. package/dist/native-host-auth.d.ts +12 -0
  59. package/dist/native-local-credential-service.d.ts +76 -0
  60. package/dist/native-local-runtime-readers.d.ts +22 -0
  61. package/dist/native-lock-observer.d.ts +12 -0
  62. package/dist/native-menu-executor.d.ts +243 -0
  63. package/dist/native-menu-model.d.ts +142 -0
  64. package/dist/native-migration-proof.d.ts +28 -0
  65. package/dist/native-migration-source.d.ts +47 -0
  66. package/dist/native-migration.d.ts +66 -0
  67. package/dist/native-quota-codec.d.ts +32 -0
  68. package/dist/native-refresh-coordinator.d.ts +240 -0
  69. package/dist/native-roster-seed.d.ts +115 -0
  70. package/dist/native-runtime-registry.d.ts +19 -0
  71. package/dist/native-runtime.d.ts +204 -0
  72. package/dist/native-settings.d.ts +6 -0
  73. package/dist/native-transient-account.d.ts +38 -0
  74. package/dist/native-ui.d.ts +45 -0
  75. package/dist/native-usage-publication.d.ts +12 -0
  76. package/dist/native-vault-admission.d.ts +8 -0
  77. package/dist/native-vault-connection.d.ts +33 -0
  78. package/dist/native-vault-paths.d.ts +13 -0
  79. package/dist/native-vault-runtime.d.ts +102 -0
  80. package/dist/native-vault-seed.d.ts +16 -0
  81. package/dist/pool-authority.d.ts +159 -0
  82. package/dist/pool-binding.d.ts +32 -0
  83. package/dist/pool-paths.d.ts +21 -0
  84. package/dist/pool-store.d.ts +32 -0
  85. package/dist/provider-error.d.ts +139 -0
  86. package/dist/quota-manager.d.ts +18 -7
  87. package/dist/relay.d.ts +14 -1
  88. package/dist/request-history.d.ts +34 -0
  89. package/dist/routing.d.ts +1 -2
  90. package/dist/sse.d.ts +81 -0
  91. package/dist/sticky-routing.d.ts +28 -0
  92. package/package.json +10 -4
  93. package/dist/accounts.js +0 -3881
  94. package/dist/auth.js +0 -185
  95. package/dist/cache1h.js +0 -97
  96. package/dist/cachekeep-registry.js +0 -124
  97. package/dist/cachekeep.js +0 -604
  98. package/dist/cch.js +0 -172
  99. package/dist/claude-code.js +0 -403
  100. package/dist/claustrum-enrollment-registry.d.ts +0 -17
  101. package/dist/claustrum-enrollment-registry.js +0 -194
  102. package/dist/claustrum-enrollment.js +0 -485
  103. package/dist/claustrum-scoped-roster.d.ts +0 -23
  104. package/dist/claustrum-scoped-roster.js +0 -194
  105. package/dist/claustrum-scoped-runtime.d.ts +0 -28
  106. package/dist/claustrum-scoped-runtime.js +0 -230
  107. package/dist/claustrum-scoped.d.ts +0 -39
  108. package/dist/claustrum-scoped.js +0 -193
  109. package/dist/claustrum.js +0 -1471
  110. package/dist/commands/account.js +0 -368
  111. package/dist/constants.js +0 -83
  112. package/dist/custom-headers.js +0 -102
  113. package/dist/dump.js +0 -616
  114. package/dist/fast.js +0 -66
  115. package/dist/json.js +0 -10
  116. package/dist/killswitch.js +0 -132
  117. package/dist/logger.js +0 -288
  118. package/dist/logging.js +0 -59
  119. package/dist/mid-conversation-output-config.js +0 -89
  120. package/dist/model-remap.js +0 -72
  121. package/dist/models.js +0 -177
  122. package/dist/network-errors.js +0 -27
  123. package/dist/oauth-profile.js +0 -60
  124. package/dist/pkce.js +0 -17
  125. package/dist/prime.js +0 -841
  126. package/dist/provider.js +0 -0
  127. package/dist/quota-header-feed.js +0 -331
  128. package/dist/quota-headers.js +0 -121
  129. package/dist/quota-manager.js +0 -856
  130. package/dist/quotas.js +0 -141
  131. package/dist/relay.js +0 -1303
  132. package/dist/routing.js +0 -94
  133. package/dist/start.js +0 -29
  134. package/dist/sticky-routing.js +0 -541
  135. package/dist/thinking-binding.js +0 -54
  136. package/dist/token-fingerprint.js +0 -8
@@ -1,194 +0,0 @@
1
- import { homedir } from 'node:os';
2
- import { isAbsolute, join, resolve } from 'node:path';
3
- import { ClaustrumEnrollmentManager, getClaustrumEnrollmentPaths, } from './claustrum-enrollment.js';
4
- const REGISTRY_SYMBOL = Symbol.for('cortexkit.anthropic-auth.claustrum-enrollments.v2');
5
- const PENDING_POLL_MS = 5_000;
6
- const RETRY_POLL_MS = 60_000;
7
- const UNAVAILABLE_RETRY_MS = 5_000;
8
- export function getHostClaustrumEnrollmentPaths(host, env = process.env, cwd = process.cwd()) {
9
- const configured = env[`${host.toUpperCase()}_ANTHROPIC_AUTH_CLAUSTRUM_ENROLLMENT_FILE`]?.trim();
10
- const tokenPath = configured
11
- ? isAbsolute(configured)
12
- ? configured
13
- : resolve(cwd, configured)
14
- : join(env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'cortexkit', 'anthropic-auth', `${host}-enrollment.json`);
15
- return getClaustrumEnrollmentPaths(tokenPath);
16
- }
17
- function registry() {
18
- const global = globalThis;
19
- if (!global[REGISTRY_SYMBOL])
20
- global[REGISTRY_SYMBOL] = new Map();
21
- return global[REGISTRY_SYMBOL];
22
- }
23
- function notify(entry, status) {
24
- entry.status = status;
25
- for (const listener of entry.leases.values())
26
- listener(status);
27
- }
28
- function schedule(entry, delayMs) {
29
- if (entry.closed || entry.timer)
30
- return;
31
- entry.timer = entry.setTimeoutImpl(() => {
32
- entry.timer = undefined;
33
- void run(entry);
34
- }, delayMs);
35
- if (typeof entry.timer === 'object' &&
36
- entry.timer &&
37
- 'unref' in entry.timer) {
38
- entry.timer.unref();
39
- }
40
- }
41
- function nextDelay(status, pollIntervalMs) {
42
- if (pollIntervalMs === 0 ||
43
- status.state === 'approved' ||
44
- status.state === 'denied' ||
45
- status.state === 'blocked') {
46
- return undefined;
47
- }
48
- if (status.state === 'pending') {
49
- return status.retryCode ? RETRY_POLL_MS : pollIntervalMs;
50
- }
51
- return UNAVAILABLE_RETRY_MS;
52
- }
53
- async function getClient(entry) {
54
- if (entry.closed)
55
- throw new Error('Claustrum enrollment registry is closed');
56
- if (entry.client)
57
- return entry.client;
58
- if (!entry.clientPromise) {
59
- entry.clientPromise = entry
60
- .connect()
61
- .then((client) => {
62
- if (entry.closed) {
63
- client.close();
64
- throw new Error('Claustrum enrollment registry is closed');
65
- }
66
- entry.client = client;
67
- return client;
68
- })
69
- .finally(() => {
70
- entry.clientPromise = undefined;
71
- });
72
- }
73
- return entry.clientPromise;
74
- }
75
- async function run(entry) {
76
- if (entry.closed)
77
- return;
78
- if (entry.run)
79
- return entry.run;
80
- entry.run = (async () => {
81
- let status;
82
- try {
83
- status = await entry.manager.reconcile();
84
- }
85
- catch {
86
- status = {
87
- state: 'unavailable',
88
- proposedName: entry.proposedName,
89
- // Unknown transport/storage errors may echo one-shot bearer material.
90
- code: 'unavailable',
91
- };
92
- // A transport or filesystem failure is not a durable ceremony verdict. Keep
93
- // retrying while the process is alive; only the manager persists protocol-terminal
94
- // outcomes such as denied, superseded, or already_consumed.
95
- if (entry.closed)
96
- return;
97
- notify(entry, status);
98
- if (entry.pollIntervalMs !== 0)
99
- schedule(entry, UNAVAILABLE_RETRY_MS);
100
- return;
101
- }
102
- if (entry.closed)
103
- return;
104
- notify(entry, status);
105
- const delay = nextDelay(status, entry.pollIntervalMs);
106
- if (delay !== undefined)
107
- schedule(entry, delay);
108
- })().finally(() => {
109
- entry.run = undefined;
110
- });
111
- return entry.run;
112
- }
113
- export function adoptClaustrumEnrollment(options) {
114
- const key = resolve(options.paths.tokenPath);
115
- const entries = registry();
116
- let entry = entries.get(key);
117
- if (entry && entry.proposedName !== options.proposedName) {
118
- throw new Error('Claustrum enrollment path is already owned by a different consumer');
119
- }
120
- if (!entry) {
121
- const connect = options.connect;
122
- const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
123
- const clearTimeoutImpl = options.clearTimeoutImpl ?? clearTimeout;
124
- const lazyClient = {
125
- enrollPropose: async (input) => {
126
- const current = entry;
127
- const client = await getClient(current);
128
- if (current.closed)
129
- throw new Error('Claustrum enrollment registry is closed');
130
- return client.enrollPropose(input);
131
- },
132
- enrollPoll: async (input) => {
133
- const current = entry;
134
- const client = await getClient(current);
135
- if (current.closed)
136
- throw new Error('Claustrum enrollment registry is closed');
137
- // Once a poll has been sent, let the manager durably collect its one-shot
138
- // approval even if release wins meanwhile. Only later calls are fenced.
139
- return client.enrollPoll(input);
140
- },
141
- };
142
- entry = {
143
- key,
144
- proposedName: options.proposedName,
145
- leases: new Map(),
146
- manager: new ClaustrumEnrollmentManager({
147
- client: lazyClient,
148
- paths: options.paths,
149
- proposedName: options.proposedName,
150
- }),
151
- connect,
152
- setTimeoutImpl,
153
- clearTimeoutImpl,
154
- pollIntervalMs: options.pollIntervalMs ?? PENDING_POLL_MS,
155
- status: { state: 'idle' },
156
- closed: false,
157
- };
158
- entries.set(key, entry);
159
- }
160
- const lease = Symbol(key);
161
- entry.leases.set(lease, options.onStatus ?? (() => { }));
162
- options.onStatus?.(entry.status);
163
- void run(entry);
164
- let released = false;
165
- return {
166
- status: () => entry?.status ?? { state: 'idle' },
167
- reconcileNow: () => (entry ? run(entry) : Promise.resolve()),
168
- resetTerminal: async () => {
169
- if (!entry)
170
- return 'idle';
171
- const result = await entry.manager.resetTerminal();
172
- if (result === 'reset') {
173
- notify(entry, { state: 'idle' });
174
- await run(entry);
175
- }
176
- return result;
177
- },
178
- release: () => {
179
- if (released || !entry)
180
- return;
181
- released = true;
182
- entry.leases.delete(lease);
183
- if (entry.leases.size > 0)
184
- return;
185
- entry.closed = true;
186
- if (entry.timer) {
187
- entry.clearTimeoutImpl(entry.timer);
188
- entry.timer = undefined;
189
- }
190
- entry.client?.close();
191
- entries.delete(key);
192
- },
193
- };
194
- }
@@ -1,485 +0,0 @@
1
- import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
- import { constants as fsConstants } from 'node:fs';
3
- import { chmod, mkdir, open, realpath, rename, stat, unlink, } from 'node:fs/promises';
4
- import { basename, dirname, extname, join } from 'node:path';
5
- import { ClaustrumCredentialError, ClaustrumClient as ClaustrumWireClient, writeEnrollmentTokenFile, } from '@cortexkit/claustrum-client';
6
- import { acquireRefreshFileLock } from './accounts.js';
7
- import { parseJsonRedacted } from './json.js';
8
- export const CLAUSTRUM_OPENCODE_ENROLLMENT_NAME = 'anthropic-auth-opencode';
9
- export const CLAUSTRUM_PI_ENROLLMENT_NAME = 'anthropic-auth-pi';
10
- const ENROLLMENT_SCHEMA = 1;
11
- const ENROLLMENT_FILE_MAX_BYTES = 16 * 1024;
12
- const ENROLLMENT_LOCK_TTL_MS = 30_000;
13
- const TOKEN_RE = /^[0-9a-f]{64}$/;
14
- export function connectClaustrumEnrollmentClient(options = {}) {
15
- return ClaustrumWireClient.connect(options);
16
- }
17
- export function getClaustrumEnrollmentPaths(tokenPath) {
18
- const extension = extname(tokenPath) || '.json';
19
- const stem = basename(tokenPath, extname(tokenPath));
20
- return {
21
- statePath: join(dirname(tokenPath), `${stem}-state${extension}`),
22
- tokenPath,
23
- };
24
- }
25
- function isRecord(value) {
26
- return typeof value === 'object' && value !== null && !Array.isArray(value);
27
- }
28
- function validTimestamp(value) {
29
- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
30
- }
31
- function validGeneration(value) {
32
- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
33
- }
34
- function decodeEnrollmentState(value) {
35
- if (!isRecord(value) ||
36
- value.version !== ENROLLMENT_SCHEMA ||
37
- typeof value.proposedName !== 'string' ||
38
- value.proposedName.length === 0) {
39
- throw new Error('invalid Claustrum enrollment state');
40
- }
41
- if (value.phase === 'pending') {
42
- if (!TOKEN_RE.test(String(value.requestSecret ?? '')) ||
43
- (value.requestId !== undefined &&
44
- (typeof value.requestId !== 'string' ||
45
- value.requestId.length === 0)) ||
46
- !validTimestamp(value.createdAt) ||
47
- !validTimestamp(value.updatedAt)) {
48
- throw new Error('invalid Claustrum enrollment state');
49
- }
50
- return {
51
- version: 1,
52
- phase: 'pending',
53
- proposedName: value.proposedName,
54
- requestSecret: value.requestSecret,
55
- ...(value.requestId !== undefined && {
56
- requestId: value.requestId,
57
- }),
58
- createdAt: value.createdAt,
59
- updatedAt: value.updatedAt,
60
- };
61
- }
62
- if (value.phase === 'approved') {
63
- if ((value.approvedName !== undefined &&
64
- (typeof value.approvedName !== 'string' ||
65
- value.approvedName.length === 0)) ||
66
- !validGeneration(value.tokenGeneration) ||
67
- !validTimestamp(value.updatedAt)) {
68
- throw new Error('invalid Claustrum enrollment state');
69
- }
70
- return {
71
- version: 1,
72
- phase: 'approved',
73
- proposedName: value.proposedName,
74
- ...(value.approvedName !== undefined && {
75
- approvedName: value.approvedName,
76
- }),
77
- tokenGeneration: value.tokenGeneration,
78
- updatedAt: value.updatedAt,
79
- };
80
- }
81
- if (value.phase === 'denied' || value.phase === 'blocked') {
82
- if ((value.errorCode !== undefined && typeof value.errorCode !== 'string') ||
83
- !validTimestamp(value.updatedAt)) {
84
- throw new Error('invalid Claustrum enrollment state');
85
- }
86
- return {
87
- version: 1,
88
- phase: value.phase,
89
- proposedName: value.proposedName,
90
- ...(value.errorCode !== undefined && {
91
- errorCode: value.errorCode,
92
- }),
93
- updatedAt: value.updatedAt,
94
- };
95
- }
96
- throw new Error('invalid Claustrum enrollment state');
97
- }
98
- function decodeTokenFile(value) {
99
- if (!isRecord(value) ||
100
- !TOKEN_RE.test(String(value.token ?? '')) ||
101
- !validGeneration(value.token_generation)) {
102
- throw new Error('invalid Claustrum enrollment token file');
103
- }
104
- return {
105
- token: value.token,
106
- token_generation: value.token_generation,
107
- };
108
- }
109
- function validateReadableSecretFile(metadata) {
110
- if (!metadata.isFile()) {
111
- throw new Error('Claustrum enrollment file must be a regular file');
112
- }
113
- if ((metadata.mode & 0o077) !== 0) {
114
- throw new Error('Claustrum enrollment file must be owner-only');
115
- }
116
- const expectedUid = process.getuid?.();
117
- if (expectedUid !== undefined && metadata.uid !== expectedUid) {
118
- throw new Error('Claustrum enrollment file must be owned by the current user');
119
- }
120
- }
121
- async function readBoundedJson(path) {
122
- let descriptor;
123
- try {
124
- descriptor = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
125
- }
126
- catch (error) {
127
- if (error.code === 'ENOENT')
128
- return undefined;
129
- throw new Error('Claustrum enrollment file could not be opened safely');
130
- }
131
- try {
132
- validateReadableSecretFile(await descriptor.stat());
133
- const source = Buffer.alloc(ENROLLMENT_FILE_MAX_BYTES + 1);
134
- const { bytesRead } = await descriptor.read(source, 0, source.byteLength, 0);
135
- if (bytesRead > ENROLLMENT_FILE_MAX_BYTES) {
136
- throw new Error('Claustrum enrollment file is too large');
137
- }
138
- return parseJsonRedacted(source.subarray(0, bytesRead).toString('utf8'));
139
- }
140
- finally {
141
- await descriptor.close();
142
- }
143
- }
144
- async function refuseWritableAncestor(parent) {
145
- let component;
146
- try {
147
- component = await realpath(parent);
148
- }
149
- catch {
150
- return;
151
- }
152
- for (;;) {
153
- const metadata = await stat(component).catch(() => undefined);
154
- if (metadata &&
155
- (metadata.mode & 0o022) !== 0 &&
156
- (metadata.mode & 0o1000) === 0) {
157
- throw new Error('Claustrum enrollment path has an unsafe writable ancestor');
158
- }
159
- const next = dirname(component);
160
- if (next === component)
161
- return;
162
- component = next;
163
- }
164
- }
165
- async function writeStateAtomic(path, state) {
166
- const parent = dirname(path);
167
- await mkdir(parent, { recursive: true, mode: 0o700 });
168
- await refuseWritableAncestor(parent);
169
- const bytes = `${JSON.stringify(state)}\n`;
170
- if (Buffer.byteLength(bytes) > ENROLLMENT_FILE_MAX_BYTES) {
171
- throw new Error('Claustrum enrollment state is too large');
172
- }
173
- const temporary = join(parent, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
174
- let descriptor;
175
- try {
176
- descriptor = await open(temporary, 'wx', 0o600);
177
- await descriptor.writeFile(bytes, 'utf8');
178
- await descriptor.sync();
179
- await descriptor.close();
180
- descriptor = undefined;
181
- await chmod(temporary, 0o600);
182
- await rename(temporary, path);
183
- }
184
- finally {
185
- await descriptor?.close().catch(() => { });
186
- await unlink(temporary).catch(() => { });
187
- }
188
- }
189
- export async function readClaustrumEnrollmentStatus(paths, proposedName = CLAUSTRUM_OPENCODE_ENROLLMENT_NAME) {
190
- const tokenValue = await readBoundedJson(paths.tokenPath);
191
- const token = tokenValue === undefined ? undefined : decodeTokenFile(tokenValue);
192
- const stateValue = await readBoundedJson(paths.statePath);
193
- const state = stateValue === undefined ? undefined : decodeEnrollmentState(stateValue);
194
- if (token) {
195
- return {
196
- state: 'approved',
197
- proposedName: state?.proposedName ?? proposedName,
198
- ...(state?.phase === 'approved' &&
199
- state.approvedName !== undefined && {
200
- approvedName: state.approvedName,
201
- }),
202
- tokenGeneration: token.token_generation,
203
- };
204
- }
205
- if (!state)
206
- return { state: 'idle' };
207
- if (state.phase === 'pending') {
208
- return {
209
- state: 'pending',
210
- proposedName: state.proposedName,
211
- ...(state.requestId !== undefined && { requestId: state.requestId }),
212
- };
213
- }
214
- if (state.phase === 'approved') {
215
- return {
216
- state: 'blocked',
217
- proposedName: state.proposedName,
218
- code: 'missing_token',
219
- };
220
- }
221
- if (state.phase === 'denied')
222
- return { state: 'denied', proposedName: state.proposedName };
223
- return {
224
- state: 'blocked',
225
- proposedName: state.proposedName,
226
- code: state.errorCode ?? 'unknown',
227
- };
228
- }
229
- /** Read fresh bearer material for a scoped operation; never publish this value. */
230
- export async function readClaustrumEnrollmentToken(tokenPath) {
231
- await refuseWritableAncestor(tokenPath);
232
- const value = await readBoundedJson(tokenPath);
233
- if (value === undefined)
234
- throw new Error('Claustrum enrollment is not configured');
235
- return decodeTokenFile(value);
236
- }
237
- function statusFromState(state) {
238
- if (state.phase === 'pending') {
239
- return {
240
- state: 'pending',
241
- proposedName: state.proposedName,
242
- ...(state.requestId !== undefined && { requestId: state.requestId }),
243
- };
244
- }
245
- if (state.phase === 'approved') {
246
- return {
247
- state: 'approved',
248
- proposedName: state.proposedName,
249
- ...(state.approvedName !== undefined && {
250
- approvedName: state.approvedName,
251
- }),
252
- tokenGeneration: state.tokenGeneration,
253
- };
254
- }
255
- if (state.phase === 'denied')
256
- return { state: 'denied', proposedName: state.proposedName };
257
- return {
258
- state: 'blocked',
259
- proposedName: state.proposedName,
260
- code: state.errorCode ?? 'unknown',
261
- };
262
- }
263
- export class ClaustrumEnrollmentManager {
264
- #client;
265
- #paths;
266
- #proposedName;
267
- #now;
268
- #mintSecret;
269
- #writeTokenFile;
270
- constructor(options) {
271
- this.#client = options.client;
272
- this.#paths = options.paths;
273
- this.#proposedName = options.proposedName;
274
- this.#now = options.now ?? Date.now;
275
- this.#mintSecret =
276
- options.mintSecret ?? (() => randomBytes(32).toString('hex'));
277
- this.#writeTokenFile = options.writeTokenFile ?? writeEnrollmentTokenFile;
278
- }
279
- async status() {
280
- return readClaustrumEnrollmentStatus(this.#paths, this.#proposedName);
281
- }
282
- async resetTerminal() {
283
- return resetClaustrumEnrollmentState(this.#paths, this.#proposedName);
284
- }
285
- async reconcile() {
286
- await mkdir(dirname(this.#paths.statePath), {
287
- recursive: true,
288
- mode: 0o700,
289
- });
290
- const lock = await acquireRefreshFileLock({
291
- name: 'ceremony',
292
- path: this.#paths.statePath,
293
- ttlMs: ENROLLMENT_LOCK_TTL_MS,
294
- renew: true,
295
- });
296
- if (!lock)
297
- return { state: 'busy' };
298
- try {
299
- const existingToken = await readBoundedJson(this.#paths.tokenPath);
300
- const token = existingToken === undefined ? undefined : decodeTokenFile(existingToken);
301
- const stateValue = await readBoundedJson(this.#paths.statePath);
302
- let state = stateValue === undefined ? undefined : decodeEnrollmentState(stateValue);
303
- if (state && state.proposedName !== this.#proposedName) {
304
- throw new Error('Claustrum enrollment state belongs to a different consumer');
305
- }
306
- if (token) {
307
- if (!state || state.phase === 'pending') {
308
- const approved = {
309
- version: 1,
310
- phase: 'approved',
311
- proposedName: state?.proposedName ?? this.#proposedName,
312
- tokenGeneration: token.token_generation,
313
- updatedAt: this.#now(),
314
- };
315
- await writeStateAtomic(this.#paths.statePath, approved);
316
- state = approved;
317
- }
318
- return {
319
- state: 'approved',
320
- proposedName: state.proposedName,
321
- ...(state.phase === 'approved' &&
322
- state.approvedName !== undefined && {
323
- approvedName: state.approvedName,
324
- }),
325
- tokenGeneration: token.token_generation,
326
- };
327
- }
328
- if (state?.phase === 'approved') {
329
- const blocked = {
330
- version: 1,
331
- phase: 'blocked',
332
- proposedName: state.proposedName,
333
- errorCode: 'missing_token',
334
- updatedAt: this.#now(),
335
- };
336
- await writeStateAtomic(this.#paths.statePath, blocked);
337
- return statusFromState(blocked);
338
- }
339
- if (state && state.phase !== 'pending')
340
- return statusFromState(state);
341
- if (!state) {
342
- const now = this.#now();
343
- state = {
344
- version: 1,
345
- phase: 'pending',
346
- proposedName: this.#proposedName,
347
- requestSecret: this.#mintSecret(),
348
- createdAt: now,
349
- updatedAt: now,
350
- };
351
- if (!TOKEN_RE.test(state.requestSecret))
352
- throw new Error('invalid minted Claustrum enrollment secret');
353
- await writeStateAtomic(this.#paths.statePath, state);
354
- }
355
- if (!state.requestId) {
356
- try {
357
- const requestSecretHash = createHash('sha256')
358
- .update(Buffer.from(state.requestSecret, 'hex'))
359
- .digest('hex');
360
- const proposed = await this.#client.enrollPropose({
361
- name: state.proposedName,
362
- requestSecretHash,
363
- });
364
- state = {
365
- ...state,
366
- requestId: proposed.requestId,
367
- updatedAt: this.#now(),
368
- };
369
- await writeStateAtomic(this.#paths.statePath, state);
370
- }
371
- catch (error) {
372
- if (error instanceof ClaustrumCredentialError) {
373
- if (error.code === 'pending_queue_full' ||
374
- error.action === 'retry') {
375
- return {
376
- state: 'pending',
377
- proposedName: state.proposedName,
378
- retryCode: error.code,
379
- };
380
- }
381
- const blocked = {
382
- version: 1,
383
- phase: 'blocked',
384
- proposedName: state.proposedName,
385
- errorCode: error.code,
386
- updatedAt: this.#now(),
387
- };
388
- await writeStateAtomic(this.#paths.statePath, blocked);
389
- return statusFromState(blocked);
390
- }
391
- throw error;
392
- }
393
- }
394
- const requestId = state.requestId;
395
- if (!requestId)
396
- return statusFromState(state);
397
- try {
398
- const outcome = await this.#client.enrollPoll({
399
- requestId,
400
- requestSecret: state.requestSecret,
401
- });
402
- if (outcome.status === 'pending')
403
- return statusFromState(state);
404
- if (outcome.status === 'denied') {
405
- const denied = {
406
- version: 1,
407
- phase: 'denied',
408
- proposedName: state.proposedName,
409
- updatedAt: this.#now(),
410
- };
411
- await writeStateAtomic(this.#paths.statePath, denied);
412
- return statusFromState(denied);
413
- }
414
- await this.#writeTokenFile(this.#paths.tokenPath, {
415
- token: outcome.token,
416
- token_generation: outcome.tokenGeneration,
417
- });
418
- const approved = {
419
- version: 1,
420
- phase: 'approved',
421
- proposedName: state.proposedName,
422
- approvedName: outcome.name,
423
- tokenGeneration: outcome.tokenGeneration,
424
- updatedAt: this.#now(),
425
- };
426
- await writeStateAtomic(this.#paths.statePath, approved);
427
- return statusFromState(approved);
428
- }
429
- catch (error) {
430
- if (!(error instanceof ClaustrumCredentialError))
431
- throw error;
432
- if (error.action === 'retry') {
433
- return {
434
- state: 'pending',
435
- proposedName: state.proposedName,
436
- requestId,
437
- retryCode: error.code,
438
- };
439
- }
440
- const blocked = {
441
- version: 1,
442
- phase: 'blocked',
443
- proposedName: state.proposedName,
444
- errorCode: error.code,
445
- updatedAt: this.#now(),
446
- };
447
- await writeStateAtomic(this.#paths.statePath, blocked);
448
- return statusFromState(blocked);
449
- }
450
- }
451
- finally {
452
- await lock.release();
453
- }
454
- }
455
- }
456
- /** Reset local terminal metadata without connecting to the credential daemon. */
457
- export async function resetClaustrumEnrollmentState(paths, proposedName) {
458
- await mkdir(dirname(paths.statePath), { recursive: true, mode: 0o700 });
459
- const lock = await acquireRefreshFileLock({
460
- name: 'ceremony',
461
- path: paths.statePath,
462
- ttlMs: ENROLLMENT_LOCK_TTL_MS,
463
- renew: true,
464
- });
465
- if (!lock)
466
- return 'busy';
467
- try {
468
- if ((await readBoundedJson(paths.tokenPath)) !== undefined)
469
- return 'refused-approved';
470
- const value = await readBoundedJson(paths.statePath);
471
- if (value === undefined)
472
- return 'idle';
473
- const state = decodeEnrollmentState(value);
474
- if (state.proposedName !== proposedName)
475
- throw new Error('Claustrum enrollment state belongs to a different consumer');
476
- if (state.phase === 'pending')
477
- return 'refused-pending';
478
- await lock.assertOwned();
479
- await unlink(paths.statePath);
480
- return 'reset';
481
- }
482
- finally {
483
- await lock.release();
484
- }
485
- }
@@ -1,23 +0,0 @@
1
- import { type AccountStorage, type OAuthAccount } from './accounts.js';
2
- import type { ClaustrumScopedAccount, ClaustrumScopedCustody } from './claustrum-scoped.js';
3
- export interface ClaustrumScopedRoster {
4
- /** The vault's conventional default record; absent when only labeled accounts exist. */
5
- primary?: ClaustrumScopedAccount;
6
- accounts: readonly OAuthAccount[];
7
- storage: AccountStorage;
8
- view: string;
9
- }
10
- /** Pure identity projection; callers must serialize discovery before persisting it. */
11
- export declare function projectClaustrumScopedRoster(storage: AccountStorage, rows: readonly ClaustrumScopedAccount[], view: string, now?: number): ClaustrumScopedRoster;
12
- /**
13
- * The lease covers LIST and commit, not just the file write: opaque view hashes
14
- * cannot tell a delayed old response from a newer one. A lost lease is checked
15
- * while holding the config lock, before any authoritative membership write.
16
- * Neither local mode nor a failed list can publish an empty inventory.
17
- */
18
- export declare function refreshClaustrumScopedRoster(options: {
19
- path: string;
20
- custody: Pick<ClaustrumScopedCustody, 'discover'>;
21
- signal?: AbortSignal;
22
- now?: () => number;
23
- }): Promise<ClaustrumScopedRoster | undefined>;