@modelprofile.com/authswitch 7.0.0 → 8.1.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/authority-contract.d.ts +1 -3
- package/dist_ts/authority-contract.js +1 -1
- package/dist_ts/authority-import-contract.d.ts +207 -0
- package/dist_ts/authority-import-contract.js +18 -0
- package/dist_ts/classes.authoritybroker.js +1 -5
- package/dist_ts/classes.authoritycli.d.ts +4 -0
- package/dist_ts/classes.authoritycli.js +149 -12
- package/dist_ts/classes.authorityclient.d.ts +7 -0
- package/dist_ts/classes.authorityclient.js +13 -1
- package/dist_ts/classes.authoritydaemon.d.ts +4 -0
- package/dist_ts/classes.authoritydaemon.js +75 -9
- package/dist_ts/classes.authoritydatabase.d.ts +10 -26
- package/dist_ts/classes.authoritydatabase.js +68 -133
- package/dist_ts/classes.authorityimport.d.ts +161 -0
- package/dist_ts/classes.authorityimport.js +638 -0
- package/dist_ts/classes.authoritymodels.d.ts +39 -43
- package/dist_ts/classes.authoritymodels.js +209 -243
- package/dist_ts/classes.authoritypreuse.js +1 -2
- package/dist_ts/classes.authorityregistry.d.ts +2 -17
- package/dist_ts/classes.authorityregistry.js +18 -74
- package/dist_ts/classes.authoritysecrets.d.ts +1 -1
- package/dist_ts/classes.authoritysecrets.js +2 -2
- package/dist_ts/classes.authorityusage.d.ts +3 -3
- package/dist_ts/classes.claudeauthority.js +3 -7
- package/dist_ts/classes.claudenative.d.ts +1 -8
- package/dist_ts/classes.claudenative.js +2 -25
- package/dist_ts/classes.codexmanaged.d.ts +1 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +6 -3
- package/dist_ts/plugins.d.ts +7 -7
- package/dist_ts/plugins.js +8 -8
- package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -3
- package/dist_ts/ts_migration/0001_authority_meta.js +29 -17
- package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
- package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
- package/dist_ts/ts_migration/index.d.ts +6 -3
- package/dist_ts/ts_migration/index.js +10 -4
- package/dist_ts/ts_migration/legacysources/authswitchstores.d.ts +2 -0
- package/dist_ts/ts_migration/legacysources/authswitchstores.js +244 -0
- package/dist_ts/ts_migration/legacysources/index.d.ts +18 -0
- package/dist_ts/ts_migration/legacysources/index.js +24 -0
- package/dist_ts/ts_migration/legacysources/material.d.ts +37 -0
- package/dist_ts/ts_migration/legacysources/material.js +120 -0
- package/dist_ts/ts_migration/legacysources/nativestores.d.ts +2 -0
- package/dist_ts/ts_migration/legacysources/nativestores.js +207 -0
- package/dist_ts/ts_migration/legacysources/shared.d.ts +124 -0
- package/dist_ts/ts_migration/legacysources/shared.js +142 -0
- package/package.json +6 -5
- package/readme.md +68 -26
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +1 -3
- package/ts/authority-import-contract.ts +216 -0
- package/ts/classes.authoritybroker.ts +0 -4
- package/ts/classes.authoritycli.ts +165 -11
- package/ts/classes.authorityclient.ts +22 -0
- package/ts/classes.authoritydaemon.ts +75 -8
- package/ts/classes.authoritydatabase.ts +70 -145
- package/ts/classes.authorityimport.ts +718 -0
- package/ts/classes.authoritymodels.ts +253 -269
- package/ts/classes.authoritypreuse.ts +0 -1
- package/ts/classes.authorityregistry.ts +20 -91
- package/ts/classes.authoritysecrets.ts +1 -1
- package/ts/classes.authorityusage.ts +3 -3
- package/ts/classes.claudeauthority.ts +2 -6
- package/ts/classes.claudenative.ts +1 -32
- package/ts/classes.codexmanaged.ts +1 -1
- package/ts/index.ts +5 -2
- package/ts/plugins.ts +7 -7
- package/ts/ts_migration/0001_authority_meta.ts +29 -17
- package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
- package/ts/ts_migration/index.ts +10 -5
- package/ts/ts_migration/legacysources/authswitchstores.ts +211 -0
- package/ts/ts_migration/legacysources/index.ts +35 -0
- package/ts/ts_migration/legacysources/material.ts +134 -0
- package/ts/ts_migration/legacysources/nativestores.ts +204 -0
- package/ts/ts_migration/legacysources/shared.ts +220 -0
- package/dist_ts/authorityrehearsal.child.d.ts +0 -1
- package/dist_ts/authorityrehearsal.child.js +0 -395
- package/dist_ts/classes.authoritybackup.d.ts +0 -134
- package/dist_ts/classes.authoritybackup.js +0 -769
- package/dist_ts/classes.authorityrehearsal.d.ts +0 -127
- package/dist_ts/classes.authorityrehearsal.js +0 -632
- package/ts/authorityrehearsal.child.ts +0 -330
- package/ts/classes.authoritybackup.ts +0 -780
- package/ts/classes.authorityrehearsal.ts +0 -625
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
assertStoredAuthorityBinding, assertStoredAuthorityClaudeHandoff, assertStoredAuthorityClaudeHome,
|
|
9
9
|
assertStoredAuthorityEnrollment, assertStoredAuthorityGrant,
|
|
10
10
|
assertStoredAuthorityHandoff, assertStoredAuthorityMigrationLedger, assertStoredAuthorityOperation,
|
|
11
|
-
assertStoredAuthorityUsage,
|
|
11
|
+
assertStoredAuthorityUsage, grantRefresherTransferRefusal, transfersGrantRefresher,
|
|
12
12
|
type IStoredAuthorityAccount, type IStoredAuthorityBinding, type IStoredAuthorityClaudeHandoff,
|
|
13
13
|
type IStoredAuthorityClaudeHome, type IStoredAuthorityCodexHome, type IStoredAuthorityEnrollment,
|
|
14
14
|
type IStoredAuthorityEvent, type IStoredAuthorityGrant, type IStoredAuthorityHandoff,
|
|
@@ -16,9 +16,7 @@ import {
|
|
|
16
16
|
type IStoredAuthorityOperation, type IStoredAuthorityPreuseOperation, type IStoredAuthorityUsage,
|
|
17
17
|
} from './classes.authoritymodels.js';
|
|
18
18
|
import type { IAuthSwitchStoredUsage, IAuthSwitchUsageContext } from './classes.authorityusage.js';
|
|
19
|
-
import {
|
|
20
|
-
assertAuthorityStoredRecord, type IAuthorityStoredRecord } from './classes.authorityregistry.js';
|
|
21
|
-
import type { IAuthorityBackupSnapshotAttempt, IAuthorityBackupSourceCapsule } from './classes.authoritybackup.js';
|
|
19
|
+
import { authorityModelRegistry } from './classes.authorityregistry.js';
|
|
22
20
|
import { runAuthorityMigrations } from './ts_migration/index.js';
|
|
23
21
|
|
|
24
22
|
export interface IAuthSwitchAuthorityDatabaseOptions {
|
|
@@ -26,15 +24,8 @@ export interface IAuthSwitchAuthorityDatabaseOptions {
|
|
|
26
24
|
socketPath: string;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
export interface IAuthoritySnapshotCaptureOptions {
|
|
30
|
-
/** Recreate capsules on every transaction retry from a source already held under qualified exclusion. */
|
|
31
|
-
sourceFactory: () => AsyncIterable<IAuthorityBackupSourceCapsule>;
|
|
32
|
-
signal?: AbortSignal;
|
|
33
|
-
pageSize?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
27
|
type TEventKind = IStoredAuthorityEvent['kind'];
|
|
37
|
-
type TSession = plugins.
|
|
28
|
+
type TSession = plugins.nosqldb.SmartdataSession;
|
|
38
29
|
type TStoredAuthorityDeviceOperationAdmission = Omit<IStoredAuthorityDeviceOperation, 'expectedGrantGeneration'>;
|
|
39
30
|
type TClaudeHandoffDraft = {
|
|
40
31
|
home: IStoredAuthorityClaudeHome;
|
|
@@ -72,10 +63,9 @@ const storedOperation = (stored: Parameters<typeof AuthSwitchAuthorityOperationM
|
|
|
72
63
|
|
|
73
64
|
/** Separate account, grant, binding and journal records; the only global record is a revision counter. */
|
|
74
65
|
export class AuthSwitchAuthorityDatabase {
|
|
75
|
-
private engine?: plugins.
|
|
76
|
-
private database?: plugins.
|
|
66
|
+
private engine?: plugins.nosqldbEngine.LocalNoSqlDb;
|
|
67
|
+
private database?: plugins.nosqldb.SmartdataDb;
|
|
77
68
|
private started = false;
|
|
78
|
-
private mode: 'normal' | 'closed_restore' | null = null;
|
|
79
69
|
|
|
80
70
|
constructor(private readonly options: IAuthSwitchAuthorityDatabaseOptions) {}
|
|
81
71
|
|
|
@@ -88,20 +78,18 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
88
78
|
await plugins.fs.promises.chmod(pathArg, 0o700);
|
|
89
79
|
}
|
|
90
80
|
|
|
91
|
-
public async start(
|
|
81
|
+
public async start(): Promise<void> {
|
|
92
82
|
if (this.started || this.engine || this.database) throw new Error('Authswitch database is already starting or started.');
|
|
93
|
-
const mode = options.mode ?? 'normal';
|
|
94
|
-
if (mode !== 'normal' && mode !== 'closed_restore') throw new Error('Invalid authority database start mode.');
|
|
95
83
|
await this.privateDirectory(this.options.dataDirectory);
|
|
96
84
|
await this.privateDirectory(plugins.path.dirname(this.options.socketPath));
|
|
97
|
-
const engine = new plugins.
|
|
85
|
+
const engine = new plugins.nosqldbEngine.LocalNoSqlDb({ folderPath: this.options.dataDirectory, socketPath: this.options.socketPath });
|
|
98
86
|
this.engine = engine;
|
|
99
87
|
try {
|
|
100
88
|
const connection = await engine.start();
|
|
101
|
-
const database = new plugins.
|
|
89
|
+
const database = new plugins.nosqldb.SmartdataDb({ mongoDbUrl: connection.connectionUri, mongoDbName: 'authswitch' });
|
|
102
90
|
this.database = database;
|
|
103
91
|
await database.init();
|
|
104
|
-
// The
|
|
92
|
+
// The LocalNoSqlDb root lease excludes another engine during this catalog admission.
|
|
105
93
|
const inventory = await database.inspectNamespace({ maxCollections: 1024,
|
|
106
94
|
documentCountUpTo: 0, timeoutMs: 30_000 });
|
|
107
95
|
const names = new Set(authorityModelRegistry.map(entry => entry.name));
|
|
@@ -111,10 +99,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
111
99
|
}
|
|
112
100
|
}
|
|
113
101
|
const fresh = inventory.collections.length === 0;
|
|
114
|
-
if (
|
|
115
|
-
throw new Error('Closed authority restore requires an empty database.');
|
|
116
|
-
}
|
|
117
|
-
if (mode === 'normal' && !fresh) {
|
|
102
|
+
if (!fresh) {
|
|
118
103
|
if (!inventory.collections.some(item => item.collectionName === 'authswitch_authority_meta')) {
|
|
119
104
|
throw new Error('Existing authswitch authority database has no metadata collection.');
|
|
120
105
|
}
|
|
@@ -122,19 +107,16 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
122
107
|
}
|
|
123
108
|
await Promise.all(authorityModelRegistry.map(entry => entry.initialize(database)));
|
|
124
109
|
await Promise.all(authorityModelRegistry.map(entry => entry.prepare()));
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
throw new Error('Existing authswitch authority metadata is missing.');
|
|
135
|
-
}
|
|
110
|
+
if (fresh) {
|
|
111
|
+
const initial: IStoredAuthorityMeta = {
|
|
112
|
+
id: 'authswitch-authority',
|
|
113
|
+
epoch: plugins.crypto.randomUUID(), revision: 0, updateId: plugins.crypto.randomUUID(),
|
|
114
|
+
};
|
|
115
|
+
const result = await AuthSwitchAuthorityMetaModel.exact.insert(initial);
|
|
116
|
+
if (result.status !== 'inserted') throw new Error('Authswitch authority could not initialize.');
|
|
117
|
+
} else if (!await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' })) {
|
|
118
|
+
throw new Error('Existing authswitch authority metadata is missing.');
|
|
136
119
|
}
|
|
137
|
-
this.mode = mode;
|
|
138
120
|
this.started = true;
|
|
139
121
|
} catch (error) {
|
|
140
122
|
try { await this.stop(); } catch { /* Preserve the original startup error. */ }
|
|
@@ -142,7 +124,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
142
124
|
}
|
|
143
125
|
}
|
|
144
126
|
|
|
145
|
-
private db(): plugins.
|
|
127
|
+
private db(): plugins.nosqldb.SmartdataDb {
|
|
146
128
|
if (!this.started || !this.database) throw new Error('Authswitch database is unavailable.');
|
|
147
129
|
return this.database;
|
|
148
130
|
}
|
|
@@ -156,70 +138,6 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
156
138
|
} finally { await session.close(); }
|
|
157
139
|
}
|
|
158
140
|
|
|
159
|
-
/** One repeatable SmartData transaction per capture attempt; portable staging happens inside visit. */
|
|
160
|
-
public async captureBackupSnapshot(options: IAuthoritySnapshotCaptureOptions,
|
|
161
|
-
visit: (attempt: IAuthorityBackupSnapshotAttempt) => Promise<void>): Promise<void> {
|
|
162
|
-
if (typeof options.sourceFactory !== 'function' || typeof visit !== 'function') {
|
|
163
|
-
throw new Error('Authority backup capture requires an exact source and visitor.');
|
|
164
|
-
}
|
|
165
|
-
await this.transaction(async session => {
|
|
166
|
-
options.signal?.throwIfAborted();
|
|
167
|
-
const stored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' },
|
|
168
|
-
{ session, signal: options.signal });
|
|
169
|
-
if (!stored) throw new Error('Authority backup metadata is absent.');
|
|
170
|
-
const meta = AuthSwitchAuthorityMetaModel.exact.toPersisted(stored);
|
|
171
|
-
if (meta.admission !== 'open') throw new Error('Closed restored authority cannot publish a backup.');
|
|
172
|
-
let recordsComplete = false;
|
|
173
|
-
let sourcesComplete = false;
|
|
174
|
-
const records = async function* (): AsyncGenerator<IAuthorityStoredRecord> {
|
|
175
|
-
yield* authorityStoredRecords({ session, signal: options.signal, pageSize: options.pageSize });
|
|
176
|
-
recordsComplete = true;
|
|
177
|
-
};
|
|
178
|
-
const sources = async function* (): AsyncGenerator<IAuthorityBackupSourceCapsule> {
|
|
179
|
-
for await (const source of options.sourceFactory()) {
|
|
180
|
-
options.signal?.throwIfAborted();
|
|
181
|
-
yield source;
|
|
182
|
-
}
|
|
183
|
-
sourcesComplete = true;
|
|
184
|
-
};
|
|
185
|
-
await visit({ epoch: meta.epoch, revision: meta.revision, records: records(), sources: sources() });
|
|
186
|
-
if (!recordsComplete || !sourcesComplete) {
|
|
187
|
-
throw new Error('Authority backup visitor did not consume the complete pinned inventory.');
|
|
188
|
-
}
|
|
189
|
-
return true;
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Reopened closed restore readback uses the same exhaustive registry and stable keyset order. */
|
|
194
|
-
public readStoredRecords(options: { signal?: AbortSignal; pageSize?: number } = {}): AsyncIterable<IAuthorityStoredRecord> {
|
|
195
|
-
this.db();
|
|
196
|
-
return authorityStoredRecords(options);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
public async restoreClosedMeta(meta: IStoredAuthorityMeta): Promise<void> {
|
|
200
|
-
if (this.mode !== 'closed_restore') throw new Error('Authority database is not a closed restore target.');
|
|
201
|
-
const record: IAuthorityStoredRecord = { model: 'authswitch_authority_meta', id: meta.id, value: meta };
|
|
202
|
-
assertAuthorityStoredRecord(record);
|
|
203
|
-
if (meta.admission !== 'closed_restored') throw new Error('Restored authority metadata must remain closed.');
|
|
204
|
-
await authorityModel(record.model).insert(record);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
public async insertClosedRestoredRecord(record: IAuthorityStoredRecord): Promise<void> {
|
|
208
|
-
if (this.mode !== 'closed_restore' || record.model === 'authswitch_authority_meta') {
|
|
209
|
-
throw new Error('Authority database is not admitting this restored record.');
|
|
210
|
-
}
|
|
211
|
-
if ((await this.readMeta()).admission !== 'closed_restored') {
|
|
212
|
-
throw new Error('Closed restore metadata must be installed before records.');
|
|
213
|
-
}
|
|
214
|
-
assertAuthorityStoredRecord(record);
|
|
215
|
-
await authorityModel(record.model).insert(record);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
public async assertOpenAdmission(): Promise<void> {
|
|
219
|
-
const meta = await this.readMeta();
|
|
220
|
-
if (meta.admission !== 'open') throw new Error('Restored authority admission is closed pending ownership reconciliation.');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
141
|
public async readMeta(): Promise<IStoredAuthorityMeta> {
|
|
224
142
|
this.db();
|
|
225
143
|
const stored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' });
|
|
@@ -257,7 +175,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
257
175
|
return stored.map(item => AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(item));
|
|
258
176
|
}
|
|
259
177
|
|
|
260
|
-
/** Home path identity is hashed by the caller; no vendor files or raw paths enter
|
|
178
|
+
/** Home path identity is hashed by the caller; no vendor files or raw paths enter NoSQLDB. */
|
|
261
179
|
public async reserveCodexHome(accountId: string, grantId: string, scopeId: string, homeId: string,
|
|
262
180
|
updateId: string): Promise<{ newlyRegistered: boolean }> {
|
|
263
181
|
try {
|
|
@@ -285,7 +203,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
285
203
|
return { newlyRegistered: true };
|
|
286
204
|
});
|
|
287
205
|
} catch (error) {
|
|
288
|
-
if (error instanceof plugins.
|
|
206
|
+
if (error instanceof plugins.nosqldb.SmartdataExactPersistenceError
|
|
289
207
|
&& (error.code === 'ambiguous_write' || error.code === 'unique_conflict')) {
|
|
290
208
|
const assigned = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId });
|
|
291
209
|
if (assigned?.accountId === accountId && assigned.grantId === grantId && assigned.scopeId === scopeId
|
|
@@ -315,7 +233,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
315
233
|
return true;
|
|
316
234
|
});
|
|
317
235
|
} catch (error) {
|
|
318
|
-
if (!(error instanceof plugins.
|
|
236
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
319
237
|
const current = await this.readCodexHome(homeId);
|
|
320
238
|
if (current?.updateId === updateId && JSON.stringify(current.activeRun) === JSON.stringify(activeRun)) return;
|
|
321
239
|
throw new Error('Managed Codex home run outcome is unknown.');
|
|
@@ -428,11 +346,6 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
428
346
|
if (!sameRequest(existing)) throw new Error('Account operation ID belongs to a different request.');
|
|
429
347
|
return { operation: existing, created: false };
|
|
430
348
|
}
|
|
431
|
-
const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne(
|
|
432
|
-
{ id: 'authswitch-authority' }, { session });
|
|
433
|
-
if (!metaStored || AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored).admission !== 'open') {
|
|
434
|
-
throw new Error('Authswitch authority admission is closed.');
|
|
435
|
-
}
|
|
436
349
|
let expectedGrantGeneration: number | null = null;
|
|
437
350
|
if (intent.kind === 'reauth_openai') {
|
|
438
351
|
const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne(
|
|
@@ -456,7 +369,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
456
369
|
return { operation, created: true };
|
|
457
370
|
});
|
|
458
371
|
} catch (error) {
|
|
459
|
-
if (!(error instanceof plugins.
|
|
372
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
460
373
|
const stored = await this.readOperation(intent.id);
|
|
461
374
|
if (stored && sameRequest(stored)) {
|
|
462
375
|
return { operation: stored, created: stored.updateId === intent.updateId };
|
|
@@ -468,13 +381,8 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
468
381
|
/** Revalidate the durable owner and exact reauthentication generation after mutable runtime preparation. */
|
|
469
382
|
public async confirmOperationMayStart(operationId: string): Promise<IStoredAuthorityDeviceOperation> {
|
|
470
383
|
return this.transaction(async session => {
|
|
471
|
-
const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne(
|
|
472
|
-
{ id: 'authswitch-authority' }, { session });
|
|
473
384
|
const operationStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne(
|
|
474
385
|
{ id: operationId }, { session });
|
|
475
|
-
if (!metaStored || AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored).admission !== 'open') {
|
|
476
|
-
throw new Error('Authswitch authority admission is closed.');
|
|
477
|
-
}
|
|
478
386
|
if (!operationStored) throw new Error('Account operation was not found.');
|
|
479
387
|
const operation = storedOperation(operationStored);
|
|
480
388
|
if (operation.kind === 'preuse_openai' || operation.state !== 'starting') {
|
|
@@ -528,7 +436,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
528
436
|
if (result.status !== 'transitioned') throw new Error('Account operation changed concurrently.');
|
|
529
437
|
return draft;
|
|
530
438
|
} catch (error) {
|
|
531
|
-
if (!(error instanceof plugins.
|
|
439
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
532
440
|
const stored = await this.readOperation(operationId);
|
|
533
441
|
if (stored?.kind !== 'preuse_openai' && stored?.updateId === updateId) return stored;
|
|
534
442
|
throw new Error('Account operation transition outcome is unknown.');
|
|
@@ -569,7 +477,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
569
477
|
return { operation, created: true };
|
|
570
478
|
});
|
|
571
479
|
} catch (error) {
|
|
572
|
-
if (!(error instanceof plugins.
|
|
480
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
573
481
|
const stored = await this.readOperation(operation.id);
|
|
574
482
|
if (stored && sameRequest(stored) && stored.updateId === operation.updateId) {
|
|
575
483
|
return { operation: stored, created: true };
|
|
@@ -611,7 +519,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
611
519
|
return draft;
|
|
612
520
|
});
|
|
613
521
|
} catch (error) {
|
|
614
|
-
if (!(error instanceof plugins.
|
|
522
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
615
523
|
const stored = await this.readOperation(operationId);
|
|
616
524
|
if (stored?.kind === 'preuse_openai' && stored.updateId === updateId
|
|
617
525
|
&& stored.state === 'request_may_have_started') return stored;
|
|
@@ -648,7 +556,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
648
556
|
if (result.status !== 'transitioned') throw new Error('Preuse operation changed concurrently.');
|
|
649
557
|
return draft;
|
|
650
558
|
} catch (error) {
|
|
651
|
-
if (!(error instanceof plugins.
|
|
559
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
652
560
|
const stored = await this.readOperation(operationId);
|
|
653
561
|
if (stored?.kind === 'preuse_openai' && stored.updateId === updateId) return stored;
|
|
654
562
|
throw new Error('Preuse operation transition outcome is unknown.');
|
|
@@ -665,6 +573,19 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
665
573
|
return stored.map(item => AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(item));
|
|
666
574
|
}
|
|
667
575
|
|
|
576
|
+
/** Every migration ledger row in id order, one bounded page at a time. */
|
|
577
|
+
public async migrationLedgerPage(after: string | null,
|
|
578
|
+
limit = 128): Promise<{ rows: IStoredAuthorityMigrationLedger[]; nextCursor: string | null }> {
|
|
579
|
+
this.db();
|
|
580
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid migration page size.');
|
|
581
|
+
const stored = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStored({
|
|
582
|
+
filter: after ? { id: { $gt: after } } : {}, sort: { id: 1 }, limit: limit + 1,
|
|
583
|
+
});
|
|
584
|
+
const rows = stored.slice(0, limit)
|
|
585
|
+
.map(item => AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(item));
|
|
586
|
+
return { rows, nextCursor: stored.length > limit ? rows[rows.length - 1].id : null };
|
|
587
|
+
}
|
|
588
|
+
|
|
668
589
|
public async readAccountAndPrimaryGrant(id: string): Promise<{
|
|
669
590
|
account: IStoredAuthorityAccount | null; grant: IStoredAuthorityGrant | null;
|
|
670
591
|
}> {
|
|
@@ -806,7 +727,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
806
727
|
return true;
|
|
807
728
|
});
|
|
808
729
|
} catch (error) {
|
|
809
|
-
if (!(error instanceof plugins.
|
|
730
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError)) throw error;
|
|
810
731
|
if (error.code === 'unique_conflict') return false;
|
|
811
732
|
if (error.code !== 'ambiguous_write') throw error;
|
|
812
733
|
const [stored, event] = await Promise.all([
|
|
@@ -1065,9 +986,8 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1065
986
|
if (accountStored && accountStored.primaryGrantId !== grantId) {
|
|
1066
987
|
throw new Error('Primary grant identity cannot be replaced.');
|
|
1067
988
|
}
|
|
1068
|
-
if (grantStored &&
|
|
1069
|
-
|
|
1070
|
-
throw new Error('Native ownership requires a verified handoff.');
|
|
989
|
+
if (grantStored && transfersGrantRefresher(grantStored.owner, draft.grant.owner)) {
|
|
990
|
+
throw new Error(grantRefresherTransferRefusal(grantStored.owner, draft.grant.owner));
|
|
1071
991
|
}
|
|
1072
992
|
await this.persistAccount(session, accountStored, draft.account);
|
|
1073
993
|
await this.persistGrant(session, grantStored, draft.grant);
|
|
@@ -1076,7 +996,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1076
996
|
return { meta, ...draft };
|
|
1077
997
|
});
|
|
1078
998
|
} catch (error) {
|
|
1079
|
-
if (!(error instanceof plugins.
|
|
999
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1080
1000
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1081
1001
|
const { account, grant } = await this.readAccountAndPrimaryGrant(accountId);
|
|
1082
1002
|
if (event && account?.updateId === updateId && grant?.updateId === updateId) {
|
|
@@ -1141,7 +1061,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1141
1061
|
return { ...draft, operation: completed };
|
|
1142
1062
|
});
|
|
1143
1063
|
} catch (error) {
|
|
1144
|
-
if (!(error instanceof plugins.
|
|
1064
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1145
1065
|
const [event, operation, pair] = await Promise.all([
|
|
1146
1066
|
AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId }),
|
|
1147
1067
|
this.readOperation(operationId), this.readAccountAndPrimaryGrant(accountId),
|
|
@@ -1176,7 +1096,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1176
1096
|
if (grant.purpose === 'claude_host_native' && draft.state === 'exchange_may_have_been_sent'
|
|
1177
1097
|
&& grant.state !== 'exchange_may_have_been_sent'
|
|
1178
1098
|
&& !(await this.hasVerifiedClaudeGrantReceipt(account.id, grant.id, session))) {
|
|
1179
|
-
throw new Error('Claude refresh requires a verified import
|
|
1099
|
+
throw new Error('Claude refresh requires a verified import receipt.');
|
|
1180
1100
|
}
|
|
1181
1101
|
const accountDraft: IStoredAuthorityAccount = { ...account, revision: account.revision + 1,
|
|
1182
1102
|
statusObservedAt: draft.statusObservedAt, updateId };
|
|
@@ -1187,7 +1107,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1187
1107
|
return { meta, account: accountDraft, grant: draft };
|
|
1188
1108
|
});
|
|
1189
1109
|
} catch (error) {
|
|
1190
|
-
if (!(error instanceof plugins.
|
|
1110
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1191
1111
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1192
1112
|
const grant = await this.readGrant(grantId);
|
|
1193
1113
|
const account = grant ? await this.readAccount(grant.accountId) : null;
|
|
@@ -1216,6 +1136,10 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1216
1136
|
|| draft.providerId !== account.providerId || draft.updateId !== updateId) {
|
|
1217
1137
|
throw new Error('Secondary grant identity is invalid.');
|
|
1218
1138
|
}
|
|
1139
|
+
// A secondary grant transfers its refresher by a verified handoff as well; one token, one refresher.
|
|
1140
|
+
if (grant && transfersGrantRefresher(grant.owner, draft.owner)) {
|
|
1141
|
+
throw new Error(grantRefresherTransferRefusal(grant.owner, draft.owner));
|
|
1142
|
+
}
|
|
1219
1143
|
await this.persistGrant(session, grantStored, draft);
|
|
1220
1144
|
const updatedAccount = { ...account, revision: account.revision + 1,
|
|
1221
1145
|
statusObservedAt: draft.statusObservedAt, updateId };
|
|
@@ -1225,7 +1149,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1225
1149
|
return { account: updatedAccount, grant: draft };
|
|
1226
1150
|
});
|
|
1227
1151
|
} catch (error) {
|
|
1228
|
-
if (!(error instanceof plugins.
|
|
1152
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1229
1153
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1230
1154
|
const account = await this.readAccount(accountId);
|
|
1231
1155
|
const grant = await this.readGrant(grantId);
|
|
@@ -1315,7 +1239,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1315
1239
|
return removed;
|
|
1316
1240
|
});
|
|
1317
1241
|
} catch (error) {
|
|
1318
|
-
if (!(error instanceof plugins.
|
|
1242
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1319
1243
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1320
1244
|
const account = await this.readAccount(accountId);
|
|
1321
1245
|
if (event && account?.removed && account.updateId === updateId) return account;
|
|
@@ -1341,7 +1265,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1341
1265
|
return { meta, account: draft };
|
|
1342
1266
|
});
|
|
1343
1267
|
} catch (error) {
|
|
1344
|
-
if (!(error instanceof plugins.
|
|
1268
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1345
1269
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1346
1270
|
const account = await this.readAccount(accountId);
|
|
1347
1271
|
if (event && account?.updateId === updateId) return { meta: await this.readMeta(), account };
|
|
@@ -1386,7 +1310,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1386
1310
|
return { meta, binding: draft };
|
|
1387
1311
|
});
|
|
1388
1312
|
} catch (error) {
|
|
1389
|
-
if (!(error instanceof plugins.
|
|
1313
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1390
1314
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1391
1315
|
const binding = await this.readBinding(bindingId);
|
|
1392
1316
|
if (event && binding?.updateId === updateId) return { meta: await this.readMeta(), binding };
|
|
@@ -1410,7 +1334,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1410
1334
|
return true;
|
|
1411
1335
|
});
|
|
1412
1336
|
} catch (error) {
|
|
1413
|
-
if (!(error instanceof plugins.
|
|
1337
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1414
1338
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1415
1339
|
const binding = await this.readBinding(bindingId);
|
|
1416
1340
|
// Every bind mints a fresh random capability. An event plus absence or a different
|
|
@@ -1458,7 +1382,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1458
1382
|
return draft;
|
|
1459
1383
|
});
|
|
1460
1384
|
} catch (error) {
|
|
1461
|
-
if (!(error instanceof plugins.
|
|
1385
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1462
1386
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1463
1387
|
const enrollment = await this.readEnrollment(enrollmentId);
|
|
1464
1388
|
if (event && enrollment?.updateId === updateId) return enrollment;
|
|
@@ -1540,7 +1464,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1540
1464
|
return draft;
|
|
1541
1465
|
});
|
|
1542
1466
|
} catch (error) {
|
|
1543
|
-
if (!(error instanceof plugins.
|
|
1467
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1544
1468
|
const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
|
|
1545
1469
|
const handoff = await this.readHandoff(handoffId);
|
|
1546
1470
|
const grant = await this.readGrant(grantId);
|
|
@@ -1563,7 +1487,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1563
1487
|
return stored ? AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(stored) : null;
|
|
1564
1488
|
}
|
|
1565
1489
|
|
|
1566
|
-
/** A
|
|
1490
|
+
/** A verified backend import receipt, not an account snapshot, authorizes an inactive Claude grant. */
|
|
1567
1491
|
public async hasVerifiedClaudeGrantReceipt(accountId: string, grantId: string,
|
|
1568
1492
|
session?: TSession): Promise<boolean> {
|
|
1569
1493
|
this.db();
|
|
@@ -1575,7 +1499,6 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1575
1499
|
filter: { grantId, status }, sort: { id: 1 }, limit: 128, session,
|
|
1576
1500
|
});
|
|
1577
1501
|
if (stored.some(item => item.accountId === accountId
|
|
1578
|
-
&& item.backupDigest !== null
|
|
1579
1502
|
&& ['authswitch_stash', 'authswitch_backup', 'claude_native'].includes(item.sourceKind))) return true;
|
|
1580
1503
|
if (stored.length === 128) throw new Error('Claude grant has too many migration receipts to verify.');
|
|
1581
1504
|
}
|
|
@@ -1623,7 +1546,6 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1623
1546
|
const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
|
|
1624
1547
|
const current = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId }, { session });
|
|
1625
1548
|
if (!receiptStored || !['verified', 'complete'].includes(receiptStored.status)
|
|
1626
|
-
|| receiptStored.backupDigest === null
|
|
1627
1549
|
|| receiptStored.sourceKind !== 'claude_native' || receiptStored.sourcePathHash !== homeId
|
|
1628
1550
|
|| receiptStored.sourceDigest !== sourceDigest || receiptStored.accountId !== accountId
|
|
1629
1551
|
|| receiptStored.grantId !== grantId || !accountStored || accountStored.removed
|
|
@@ -1649,7 +1571,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1649
1571
|
return home;
|
|
1650
1572
|
});
|
|
1651
1573
|
} catch (error) {
|
|
1652
|
-
if (!(error instanceof plugins.
|
|
1574
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError)
|
|
1653
1575
|
|| !['ambiguous_write', 'unique_conflict'].includes(error.code)) throw error;
|
|
1654
1576
|
const home = await this.readClaudeHome(homeId);
|
|
1655
1577
|
if (home?.updateId === updateId && home.activeAccountId === accountId && home.activeGrantId === grantId) return home;
|
|
@@ -1772,7 +1694,6 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1772
1694
|
});
|
|
1773
1695
|
if (receipts.length === 128) throw new Error('Claude grant receipt set is unbounded.');
|
|
1774
1696
|
verifiedIncoming ||= receipts.some(item => item.accountId === incoming.accountId
|
|
1775
|
-
&& item.backupDigest !== null
|
|
1776
1697
|
&& ['authswitch_stash', 'authswitch_backup', 'claude_native'].includes(item.sourceKind));
|
|
1777
1698
|
}
|
|
1778
1699
|
if (draft.handoff.phase !== 'reserved' || home.pendingOperationId !== null
|
|
@@ -1843,7 +1764,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1843
1764
|
return draft;
|
|
1844
1765
|
});
|
|
1845
1766
|
} catch (error) {
|
|
1846
|
-
if (!(error instanceof plugins.
|
|
1767
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1847
1768
|
const context = await this.readClaudeHandoffContext(operationId);
|
|
1848
1769
|
const first = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: eventIds[0] });
|
|
1849
1770
|
const second = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: eventIds[1] });
|
|
@@ -1860,7 +1781,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1860
1781
|
}
|
|
1861
1782
|
}
|
|
1862
1783
|
|
|
1863
|
-
/** Ledger transitions never silently replace an active credential or waive
|
|
1784
|
+
/** Ledger transitions never silently replace an active credential or waive the verified import. */
|
|
1864
1785
|
public async changeMigrationLedger(updateId: string, ledgerId: string,
|
|
1865
1786
|
change: (current: IStoredAuthorityMigrationLedger | null) => IStoredAuthorityMigrationLedger): Promise<IStoredAuthorityMigrationLedger> {
|
|
1866
1787
|
try {
|
|
@@ -1869,7 +1790,12 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1869
1790
|
const draft = change(current ? structuredClone(AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(current)) : null);
|
|
1870
1791
|
if (draft.id !== ledgerId || draft.updateId !== updateId) throw new Error('Migration ledger identity is invalid.');
|
|
1871
1792
|
assertStoredAuthorityMigrationLedger(draft);
|
|
1872
|
-
|
|
1793
|
+
// A source may only be considered migrated after a verified import bound to an account, a grant and a
|
|
1794
|
+
// source digest. This deliberately tightens a flow that has no production caller and has never run.
|
|
1795
|
+
if (draft.status === 'complete' && (current?.status !== 'verified'
|
|
1796
|
+
|| current.accountId !== draft.accountId || current.grantId !== draft.grantId)) {
|
|
1797
|
+
throw new Error('Migration completion requires a verified import of the same account and grant.');
|
|
1798
|
+
}
|
|
1873
1799
|
if (current) {
|
|
1874
1800
|
if (current.status === 'complete' || current.version !== draft.version
|
|
1875
1801
|
|| current.sourceKind !== draft.sourceKind || current.sourcePathHash !== draft.sourcePathHash
|
|
@@ -1888,7 +1814,7 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1888
1814
|
return draft;
|
|
1889
1815
|
});
|
|
1890
1816
|
} catch (error) {
|
|
1891
|
-
if (!(error instanceof plugins.
|
|
1817
|
+
if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
|
|
1892
1818
|
const ledger = await this.readMigrationLedger(ledgerId);
|
|
1893
1819
|
if (ledger?.updateId === updateId) return ledger;
|
|
1894
1820
|
throw new Error('Authswitch migration ledger write outcome is unknown.');
|
|
@@ -1897,7 +1823,6 @@ export class AuthSwitchAuthorityDatabase {
|
|
|
1897
1823
|
|
|
1898
1824
|
public async stop(): Promise<void> {
|
|
1899
1825
|
this.started = false;
|
|
1900
|
-
this.mode = null;
|
|
1901
1826
|
const database = this.database;
|
|
1902
1827
|
const engine = this.engine;
|
|
1903
1828
|
this.database = undefined;
|