@cortexkit/anthropic-auth-core 1.23.0 → 2.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.
- package/README.md +2 -0
- package/dist/accounts.d.ts +3 -16
- package/dist/cachekeep.d.ts +9 -0
- package/dist/claude-code.d.ts +16 -0
- package/dist/claustrum-enrollment.d.ts +2 -0
- package/dist/claustrum-scoped-roster.d.ts +4 -0
- package/dist/claustrum-scoped-runtime.d.ts +3 -0
- package/dist/claustrum-scoped.d.ts +14 -0
- package/dist/claustrum.d.ts +0 -209
- package/dist/commands/account.d.ts +2 -3
- package/dist/index.d.ts +44 -1
- package/dist/index.js +29700 -38
- package/dist/internal-types/LICENSE +21 -0
- package/dist/internal-types/commands/builtins.d.ts +71 -0
- package/dist/internal-types/commands/index.d.ts +10 -0
- package/dist/internal-types/commands/menu.d.ts +61 -0
- package/dist/internal-types/commands/model.d.ts +238 -0
- package/dist/internal-types/commands/pi.d.ts +23 -0
- package/dist/internal-types/commands/seam.d.ts +72 -0
- package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
- package/dist/internal-types/logger/capture-sink.d.ts +14 -0
- package/dist/internal-types/logger/engine.d.ts +100 -0
- package/dist/internal-types/logger/index.d.ts +6 -0
- package/dist/internal-types/logger/redact.d.ts +11 -0
- package/dist/internal-types/pi-slot/index.d.ts +3 -0
- package/dist/internal-types/pi-slot/slot.d.ts +49 -0
- package/dist/internal-types/quota/codec.d.ts +11 -0
- package/dist/internal-types/quota/format.d.ts +46 -0
- package/dist/internal-types/quota/index.d.ts +10 -0
- package/dist/internal-types/quota/map.d.ts +63 -0
- package/dist/internal-types/quota/merge.d.ts +48 -0
- package/dist/internal-types/quota/projection.d.ts +64 -0
- package/dist/internal-types/store/attribution.d.ts +28 -0
- package/dist/internal-types/store/errors.d.ts +55 -0
- package/dist/internal-types/store/hooks.d.ts +12 -0
- package/dist/internal-types/store/identity.d.ts +39 -0
- package/dist/internal-types/store/index.d.ts +21 -0
- package/dist/internal-types/store/mutate.d.ts +189 -0
- package/dist/internal-types/store/pool.d.ts +196 -0
- package/dist/internal-types/store/provider-state.d.ts +131 -0
- package/dist/internal-types/store/publication.d.ts +47 -0
- package/dist/internal-types/store/pull.d.ts +36 -0
- package/dist/internal-types/store/refresh-lock.d.ts +63 -0
- package/dist/internal-types/store/refresh.d.ts +72 -0
- package/dist/internal-types/store/rows.d.ts +313 -0
- package/dist/internal-types/store/runtime.d.ts +36 -0
- package/dist/internal-types/store/schema.d.ts +443 -0
- package/dist/internal-types/store/settings.d.ts +63 -0
- package/dist/internal-types/store/torn.d.ts +78 -0
- package/dist/migration-source.d.ts +33 -0
- package/dist/models.d.ts +41 -0
- package/dist/native-account-runtime.d.ts +121 -0
- package/dist/native-account-view.d.ts +126 -0
- package/dist/native-command-effects.d.ts +59 -0
- package/dist/native-credential-validation.d.ts +42 -0
- package/dist/native-custody-activation.d.ts +45 -0
- package/dist/native-custody.d.ts +114 -0
- package/dist/native-enrollment-authority.d.ts +2 -0
- package/dist/native-host-auth-read.d.ts +3 -0
- package/dist/native-host-auth-write.d.ts +51 -0
- package/dist/native-host-auth.d.ts +12 -0
- package/dist/native-local-credential-service.d.ts +76 -0
- package/dist/native-local-runtime-readers.d.ts +22 -0
- package/dist/native-lock-observer.d.ts +12 -0
- package/dist/native-menu-executor.d.ts +243 -0
- package/dist/native-menu-model.d.ts +142 -0
- package/dist/native-migration-proof.d.ts +28 -0
- package/dist/native-migration-source.d.ts +47 -0
- package/dist/native-migration.d.ts +66 -0
- package/dist/native-quota-codec.d.ts +32 -0
- package/dist/native-refresh-coordinator.d.ts +240 -0
- package/dist/native-roster-seed.d.ts +115 -0
- package/dist/native-runtime-registry.d.ts +19 -0
- package/dist/native-runtime.d.ts +204 -0
- package/dist/native-transient-account.d.ts +38 -0
- package/dist/native-ui.d.ts +45 -0
- package/dist/native-usage-publication.d.ts +11 -0
- package/dist/native-vault-admission.d.ts +8 -0
- package/dist/native-vault-connection.d.ts +33 -0
- package/dist/native-vault-paths.d.ts +13 -0
- package/dist/native-vault-runtime.d.ts +102 -0
- package/dist/native-vault-seed.d.ts +16 -0
- package/dist/pool-authority.d.ts +159 -0
- package/dist/pool-binding.d.ts +32 -0
- package/dist/pool-paths.d.ts +21 -0
- package/dist/pool-store.d.ts +32 -0
- package/dist/provider-error.d.ts +139 -0
- package/dist/quota-manager.d.ts +18 -7
- package/dist/relay.d.ts +14 -1
- package/dist/request-history.d.ts +34 -0
- package/dist/sse.d.ts +81 -0
- package/dist/sticky-routing.d.ts +27 -0
- package/package.json +9 -4
- package/dist/accounts.js +0 -3881
- package/dist/auth.js +0 -185
- package/dist/cache1h.js +0 -97
- package/dist/cachekeep-registry.js +0 -124
- package/dist/cachekeep.js +0 -604
- package/dist/cch.js +0 -172
- package/dist/claude-code.js +0 -403
- package/dist/claustrum-enrollment-registry.d.ts +0 -17
- package/dist/claustrum-enrollment-registry.js +0 -194
- package/dist/claustrum-enrollment.js +0 -485
- package/dist/claustrum-scoped-roster.js +0 -194
- package/dist/claustrum-scoped-runtime.js +0 -230
- package/dist/claustrum-scoped.js +0 -193
- package/dist/claustrum.js +0 -1471
- package/dist/commands/account.js +0 -368
- package/dist/constants.js +0 -83
- package/dist/custom-headers.js +0 -102
- package/dist/dump.js +0 -616
- package/dist/fast.js +0 -66
- package/dist/json.js +0 -10
- package/dist/killswitch.js +0 -132
- package/dist/logger.js +0 -288
- package/dist/logging.js +0 -59
- package/dist/mid-conversation-output-config.js +0 -89
- package/dist/model-remap.js +0 -72
- package/dist/models.js +0 -177
- package/dist/network-errors.js +0 -27
- package/dist/oauth-profile.js +0 -60
- package/dist/pkce.js +0 -17
- package/dist/prime.js +0 -841
- package/dist/provider.js +0 -0
- package/dist/quota-header-feed.js +0 -331
- package/dist/quota-headers.js +0 -121
- package/dist/quota-manager.js +0 -856
- package/dist/quotas.js +0 -141
- package/dist/relay.js +0 -1303
- package/dist/routing.js +0 -94
- package/dist/start.js +0 -29
- package/dist/sticky-routing.js +0 -541
- package/dist/thinking-binding.js +0 -54
- 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
|
-
}
|