@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
|
@@ -115,7 +115,6 @@ export class AuthSwitchAuthorityPreuse {
|
|
|
115
115
|
private async admit(input: IAuthSwitchAuthorityPreuseRequest, requestHash: string,
|
|
116
116
|
requestedModel: string | null,
|
|
117
117
|
admitFresh: TAuthSwitchAuthorityPreuseFreshAdmission): Promise<IAuthSwitchPreuseOperation> {
|
|
118
|
-
await this.database.assertOpenAdmission();
|
|
119
118
|
if (this.closed) throw new Error('Authswitch preuse is closing.');
|
|
120
119
|
const prior = await this.database.readOperation(input.operationId);
|
|
121
120
|
if (prior) {
|
|
@@ -4,73 +4,40 @@ import {
|
|
|
4
4
|
AuthSwitchAuthorityClaudeHomeModel, AuthSwitchAuthorityCodexHomeModel, AuthSwitchAuthorityEnrollmentModel,
|
|
5
5
|
AuthSwitchAuthorityEventModel, AuthSwitchAuthorityGrantModel, AuthSwitchAuthorityHandoffModel,
|
|
6
6
|
AuthSwitchAuthorityMetaModel, AuthSwitchAuthorityMigrationLedgerModel, AuthSwitchAuthorityOperationModel,
|
|
7
|
-
AuthSwitchAuthorityUsageModel,
|
|
8
|
-
assertStoredAuthorityClaudeHandoff, assertStoredAuthorityClaudeHome, assertStoredAuthorityCodexHome,
|
|
9
|
-
assertStoredAuthorityEnrollment, assertStoredAuthorityEvent, assertStoredAuthorityGrant,
|
|
10
|
-
assertStoredAuthorityHandoff, assertStoredAuthorityMeta, assertStoredAuthorityMigrationLedger,
|
|
11
|
-
assertStoredAuthorityOperationDocument, assertStoredAuthorityUsage,
|
|
7
|
+
AuthSwitchAuthorityUsageModel,
|
|
12
8
|
} from './classes.authoritymodels.js';
|
|
13
9
|
|
|
14
|
-
export interface IAuthorityStoredRecord { model: string; id: string; value: unknown }
|
|
15
|
-
|
|
16
10
|
interface IAuthorityModelEntry {
|
|
17
11
|
readonly name: string;
|
|
18
|
-
initialize(db: plugins.
|
|
12
|
+
initialize(db: plugins.nosqldb.SmartdataDb): Promise<void>;
|
|
19
13
|
prepare(): Promise<void>;
|
|
20
|
-
assert(value: unknown): void;
|
|
21
|
-
page(after: string | null, limit: number, session?: plugins.smartdata.SmartdataSession,
|
|
22
|
-
signal?: AbortSignal): Promise<IAuthorityStoredRecord[]>;
|
|
23
|
-
insert(record: IAuthorityStoredRecord, session?: plugins.smartdata.SmartdataSession): Promise<void>;
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
const define = <
|
|
27
|
-
|
|
28
|
-
name: string,
|
|
29
|
-
model: { new(): TModel; init(): Promise<void>; exact: plugins.smartdata.TExact<TModel> },
|
|
30
|
-
assertDocument: (value: unknown) => asserts value is TPersisted,
|
|
31
|
-
): IAuthorityModelEntry => Object.freeze({
|
|
16
|
+
const define = (name: string, model: { init(): Promise<void>; exact: { prepare(): Promise<void> } }): IAuthorityModelEntry =>
|
|
17
|
+
Object.freeze({
|
|
32
18
|
name,
|
|
33
|
-
initialize: async (db: plugins.
|
|
34
|
-
plugins.
|
|
19
|
+
initialize: async (db: plugins.nosqldb.SmartdataDb) => {
|
|
20
|
+
plugins.nosqldb.setDefaultManagerForDoc({ db }, model);
|
|
35
21
|
await model.init();
|
|
36
22
|
},
|
|
37
23
|
prepare: () => model.exact.prepare(),
|
|
38
|
-
assert: (value: unknown) => { assertDocument(value); },
|
|
39
|
-
page: async (after: string | null, limit: number, session?: plugins.smartdata.SmartdataSession,
|
|
40
|
-
signal?: AbortSignal): Promise<IAuthorityStoredRecord[]> => {
|
|
41
|
-
const query = {
|
|
42
|
-
filter: after === null ? {} : { id: { $gt: after } }, sort: { id: 1 }, limit, session, signal,
|
|
43
|
-
} as Parameters<typeof model.exact.findStored>[0];
|
|
44
|
-
const stored = await model.exact.findStored(query);
|
|
45
|
-
return stored.map(item => {
|
|
46
|
-
const value = model.exact.toPersisted(item);
|
|
47
|
-
return { model: name, id: value.id, value };
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
insert: async (record: IAuthorityStoredRecord, session?: plugins.smartdata.SmartdataSession): Promise<void> => {
|
|
51
|
-
assertDocument(record.value);
|
|
52
|
-
if (record.value.id !== record.id) throw new Error('Authority restore record identity differs from its key.');
|
|
53
|
-
const result = await model.exact.insert(record.value as unknown as Parameters<typeof model.exact.insert>[0],
|
|
54
|
-
{ session });
|
|
55
|
-
if (result.status !== 'inserted') throw new Error('Authority restore record already exists.');
|
|
56
|
-
},
|
|
57
24
|
});
|
|
58
25
|
|
|
59
|
-
/** The sole authority collection inventory
|
|
26
|
+
/** The sole authority collection inventory: startup admits exactly these collections and prepares each one. */
|
|
60
27
|
export const authorityModelRegistry: readonly IAuthorityModelEntry[] = Object.freeze([
|
|
61
|
-
define('authswitch_authority_accounts', AuthSwitchAuthorityAccountModel
|
|
62
|
-
define('authswitch_authority_bindings', AuthSwitchAuthorityBindingModel
|
|
63
|
-
define('authswitch_authority_claude_handoffs', AuthSwitchAuthorityClaudeHandoffModel
|
|
64
|
-
define('authswitch_authority_claude_homes', AuthSwitchAuthorityClaudeHomeModel
|
|
65
|
-
define('authswitch_authority_codex_homes', AuthSwitchAuthorityCodexHomeModel
|
|
66
|
-
define('authswitch_authority_enrollments', AuthSwitchAuthorityEnrollmentModel
|
|
67
|
-
define('authswitch_authority_events', AuthSwitchAuthorityEventModel
|
|
68
|
-
define('authswitch_authority_grants', AuthSwitchAuthorityGrantModel
|
|
69
|
-
define('authswitch_authority_handoffs', AuthSwitchAuthorityHandoffModel
|
|
70
|
-
define('authswitch_authority_meta', AuthSwitchAuthorityMetaModel
|
|
71
|
-
define('authswitch_authority_migration_ledger', AuthSwitchAuthorityMigrationLedgerModel
|
|
72
|
-
define('authswitch_authority_operations', AuthSwitchAuthorityOperationModel
|
|
73
|
-
define('authswitch_authority_usage', AuthSwitchAuthorityUsageModel
|
|
28
|
+
define('authswitch_authority_accounts', AuthSwitchAuthorityAccountModel),
|
|
29
|
+
define('authswitch_authority_bindings', AuthSwitchAuthorityBindingModel),
|
|
30
|
+
define('authswitch_authority_claude_handoffs', AuthSwitchAuthorityClaudeHandoffModel),
|
|
31
|
+
define('authswitch_authority_claude_homes', AuthSwitchAuthorityClaudeHomeModel),
|
|
32
|
+
define('authswitch_authority_codex_homes', AuthSwitchAuthorityCodexHomeModel),
|
|
33
|
+
define('authswitch_authority_enrollments', AuthSwitchAuthorityEnrollmentModel),
|
|
34
|
+
define('authswitch_authority_events', AuthSwitchAuthorityEventModel),
|
|
35
|
+
define('authswitch_authority_grants', AuthSwitchAuthorityGrantModel),
|
|
36
|
+
define('authswitch_authority_handoffs', AuthSwitchAuthorityHandoffModel),
|
|
37
|
+
define('authswitch_authority_meta', AuthSwitchAuthorityMetaModel),
|
|
38
|
+
define('authswitch_authority_migration_ledger', AuthSwitchAuthorityMigrationLedgerModel),
|
|
39
|
+
define('authswitch_authority_operations', AuthSwitchAuthorityOperationModel),
|
|
40
|
+
define('authswitch_authority_usage', AuthSwitchAuthorityUsageModel),
|
|
74
41
|
]);
|
|
75
42
|
|
|
76
43
|
for (let index = 1; index < authorityModelRegistry.length; index++) {
|
|
@@ -78,41 +45,3 @@ for (let index = 1; index < authorityModelRegistry.length; index++) {
|
|
|
78
45
|
throw new Error('Authority model registry must be unique and sorted.');
|
|
79
46
|
}
|
|
80
47
|
}
|
|
81
|
-
|
|
82
|
-
export const authorityModel = (name: string): IAuthorityModelEntry => {
|
|
83
|
-
const entry = authorityModelRegistry.find(item => item.name === name);
|
|
84
|
-
if (!entry) throw new Error('Unclassified authority model cannot be read, backed up or restored.');
|
|
85
|
-
return entry;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const assertAuthorityStoredRecord = (record: IAuthorityStoredRecord): void => {
|
|
89
|
-
if (!record || typeof record.model !== 'string' || typeof record.id !== 'string' || !record.id) {
|
|
90
|
-
throw new Error('Invalid authority record identity.');
|
|
91
|
-
}
|
|
92
|
-
authorityModel(record.model).assert(record.value);
|
|
93
|
-
if (!record.value || typeof record.value !== 'object' || !('id' in record.value)
|
|
94
|
-
|| record.value.id !== record.id) throw new Error('Invalid authority record identity.');
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export async function* authorityStoredRecords(options: {
|
|
98
|
-
session?: plugins.smartdata.SmartdataSession; signal?: AbortSignal; pageSize?: number;
|
|
99
|
-
} = {}): AsyncGenerator<IAuthorityStoredRecord> {
|
|
100
|
-
const pageSize = options.pageSize ?? 64;
|
|
101
|
-
if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 256) {
|
|
102
|
-
throw new Error('Invalid authority record page size.');
|
|
103
|
-
}
|
|
104
|
-
for (const entry of authorityModelRegistry) {
|
|
105
|
-
let after: string | null = null;
|
|
106
|
-
while (true) {
|
|
107
|
-
options.signal?.throwIfAborted();
|
|
108
|
-
const page = await entry.page(after, pageSize, options.session, options.signal);
|
|
109
|
-
if (!page.length) break;
|
|
110
|
-
for (const record of page) {
|
|
111
|
-
assertAuthorityStoredRecord(record);
|
|
112
|
-
yield record;
|
|
113
|
-
}
|
|
114
|
-
after = page[page.length - 1].id;
|
|
115
|
-
if (page.length < pageSize) break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -10,7 +10,7 @@ const secretName = (accountId: string): string => {
|
|
|
10
10
|
return `authswitch.account.${accountId}`;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
/** TPM-sealed byte payloads live only as ciphertext in
|
|
13
|
+
/** TPM-sealed byte payloads live only as ciphertext in NoSQLDB. */
|
|
14
14
|
export class AuthSwitchTpmSecretCodec implements IAuthSwitchSecretCodec {
|
|
15
15
|
public async seal(accountId: string, plaintext: Uint8Array): Promise<string> {
|
|
16
16
|
const ciphertext = await plugins.smartsecret.sealSmartSecretTpm2Credential(secretName(accountId), plaintext);
|
|
@@ -58,7 +58,7 @@ export interface IAuthSwitchUsageAccess {
|
|
|
58
58
|
grantGeneration: number;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** Persist through
|
|
61
|
+
/** Persist through NoSQLDB in the authority integration. No credential belongs in this record. */
|
|
62
62
|
export interface IAuthSwitchStoredUsage {
|
|
63
63
|
context: IAuthSwitchUsageContext;
|
|
64
64
|
updateId: string;
|
|
@@ -89,7 +89,7 @@ export interface IAuthSwitchUsageStore {
|
|
|
89
89
|
/** Read only the exact login's record. The helper discards a mismatched context. */
|
|
90
90
|
read(accountId: string, loginId: string): Promise<IAuthSwitchStoredUsage | null>;
|
|
91
91
|
/**
|
|
92
|
-
* In one
|
|
92
|
+
* In one NoSQLDB transaction, re-read epoch/account/login and compare every semantic
|
|
93
93
|
* context field, compare the previous usage update ID, write next, and append a revisioned
|
|
94
94
|
* account event. The account row revision may increase in that transaction; it is not
|
|
95
95
|
* part of the usage validity key and must not invalidate the reading just published.
|
|
@@ -102,7 +102,7 @@ export interface IAuthSwitchUsageStore {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
export interface IAuthSwitchAuthorityUsageOptions {
|
|
105
|
-
/** Re-read from the owning
|
|
105
|
+
/** Re-read from the owning NoSQLDB authority. Return null for an absent login. */
|
|
106
106
|
readContext(accountId: string, loginId: string): Promise<IAuthSwitchUsageContext | null>;
|
|
107
107
|
/** Broker-owned access resolution; rejected generation may perform at most one durable refresh. */
|
|
108
108
|
resolveAccess(context: IAuthSwitchUsageContext, rejectedGrantGeneration: number | undefined,
|
|
@@ -103,14 +103,12 @@ export class ClaudeNativeAuthority {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
public async start(): Promise<void> {
|
|
106
|
-
await this.database.assertOpenAdmission();
|
|
107
106
|
await this.reconcilePending();
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
public async refreshInactiveGrant(grantId: string, rejectedGrantGeneration?: number,
|
|
111
110
|
minValidityMs = 300_000,
|
|
112
111
|
expectedGrant?: Pick<IStoredAuthorityGrant, 'revision' | 'grantGeneration' | 'ciphertext'>): Promise<void> {
|
|
113
|
-
await this.database.assertOpenAdmission();
|
|
114
112
|
if (!isHash(grantId) || this.closed || !Number.isSafeInteger(minValidityMs)
|
|
115
113
|
|| minValidityMs < 0 || minValidityMs > 3_600_000
|
|
116
114
|
|| (rejectedGrantGeneration !== undefined && (!Number.isSafeInteger(rejectedGrantGeneration)
|
|
@@ -139,7 +137,7 @@ export class ClaudeNativeAuthority {
|
|
|
139
137
|
const account = await this.database.readAccount(grant.accountId);
|
|
140
138
|
if (!account || account.removed) return;
|
|
141
139
|
if (!(await this.database.hasVerifiedClaudeGrantReceipt(account.id, grant.id))) {
|
|
142
|
-
throw new Error('Claude refresh requires a verified import
|
|
140
|
+
throw new Error('Claude refresh requires a verified import receipt.');
|
|
143
141
|
}
|
|
144
142
|
const login = await this.unsealLogin(account, grant);
|
|
145
143
|
if (rejectedGrantGeneration !== undefined && rejectedGrantGeneration > grant.grantGeneration) {
|
|
@@ -231,7 +229,7 @@ export class ClaudeNativeAuthority {
|
|
|
231
229
|
throw new Error('Inactive Claude status authority changed.');
|
|
232
230
|
}
|
|
233
231
|
if (!(await this.database.hasVerifiedClaudeGrantReceipt(view.account.id, view.grant.id))) {
|
|
234
|
-
throw new Error('Claude status requires a verified import
|
|
232
|
+
throw new Error('Claude status requires a verified import receipt.');
|
|
235
233
|
}
|
|
236
234
|
const currentLogin = this.statusLogin(view.account, await this.unsealLogin(view.account, view.grant));
|
|
237
235
|
if (!currentLogin.scopes.includes('user:profile')) {
|
|
@@ -288,7 +286,6 @@ export class ClaudeNativeAuthority {
|
|
|
288
286
|
if (this.closed || !isHash(context.accountId) || !isHash(context.loginId)) {
|
|
289
287
|
throw new Error('Invalid or closed Claude native status request.');
|
|
290
288
|
}
|
|
291
|
-
await this.database.assertOpenAdmission();
|
|
292
289
|
return this.options.adapter.withExclusiveHome(async session => {
|
|
293
290
|
const inspect = async (): Promise<{ context: IAuthSwitchUsageContext; login: IClaudeLogin } | null> => {
|
|
294
291
|
const view = await this.database.readClaudeNativeUsageView(this.options.homeId,
|
|
@@ -474,7 +471,6 @@ export class ClaudeNativeAuthority {
|
|
|
474
471
|
if (!isHash(accountId) || !isHash(loginId) || this.closed) {
|
|
475
472
|
throw new Error('Invalid or closed Claude handoff request.');
|
|
476
473
|
}
|
|
477
|
-
await this.database.assertOpenAdmission();
|
|
478
474
|
const task = this.performSwitch(accountId, loginId);
|
|
479
475
|
this.activeHandoffs.add(task);
|
|
480
476
|
try { return await task; }
|
|
@@ -87,13 +87,6 @@ export interface IClaudeNativePreparedWrite {
|
|
|
87
87
|
readonly profile: IClaudeNativeProfileProof;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/** Caller-owned copies of the original native bytes before a prepared write. Null means absent. */
|
|
91
|
-
export interface IClaudeNativePreparedSource {
|
|
92
|
-
readonly before: IClaudeNativeDigests;
|
|
93
|
-
readonly credentialBytes: Uint8Array | null;
|
|
94
|
-
readonly configBytes: Uint8Array | null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
90
|
export type TClaudeNativePrepareResult =
|
|
98
91
|
| { kind: 'prepared'; plan: IClaudeNativePreparedWrite }
|
|
99
92
|
| Extract<TClaudeNativeWriteResult, { kind: 'not_written' }>;
|
|
@@ -108,7 +101,6 @@ export interface IClaudeNativeLockedSession {
|
|
|
108
101
|
targetLogin: IClaudeNativeLogin;
|
|
109
102
|
expectedIdentity: IClaudeNativeIdentity;
|
|
110
103
|
}): Promise<TClaudeNativePrepareResult>;
|
|
111
|
-
exportPreparedSource(plan: IClaudeNativePreparedWrite): IClaudeNativePreparedSource;
|
|
112
104
|
writePreparedIfCurrent(plan: IClaudeNativePreparedWrite): TClaudeNativeWriteResult;
|
|
113
105
|
writeIfCurrent(input: {
|
|
114
106
|
expected: IClaudeNativeDigests;
|
|
@@ -567,7 +559,7 @@ export class ClaudeNativeAdapter {
|
|
|
567
559
|
return runningSessions;
|
|
568
560
|
}
|
|
569
561
|
|
|
570
|
-
/** The caller keeps its
|
|
562
|
+
/** The caller keeps its NoSQLDB journal and both ownership transactions inside this callback. */
|
|
571
563
|
public async withExclusiveHome<T>(action: (session: IClaudeNativeLockedSession) => Promise<T>): Promise<T> {
|
|
572
564
|
this.assertSupported();
|
|
573
565
|
return this.locks.holdAsync(async lease => {
|
|
@@ -730,29 +722,6 @@ class ClaudeNativeLockedSession implements IClaudeNativeLockedSession {
|
|
|
730
722
|
return { kind: 'prepared', plan };
|
|
731
723
|
}
|
|
732
724
|
|
|
733
|
-
public exportPreparedSource(plan: IClaudeNativePreparedWrite): IClaudeNativePreparedSource {
|
|
734
|
-
this.lease.assertHeld();
|
|
735
|
-
this.assertSupported();
|
|
736
|
-
const prepared = preparedWrites.get(plan);
|
|
737
|
-
if (!prepared || prepared.session !== this) throw new Error('Claude native write plan is not held by this lease.');
|
|
738
|
-
const stable = this.readStable();
|
|
739
|
-
if (!sameDigests(stable, plan.before)
|
|
740
|
-
|| (plan.sourceIdentity === null ? stable.identity !== null
|
|
741
|
-
: stable.identity === null || !sameIdentity(stable.identity, plan.sourceIdentity))) {
|
|
742
|
-
throw new Error('Claude native source changed before backup export.');
|
|
743
|
-
}
|
|
744
|
-
const original = readPair(this.credentialFile, this.configFile);
|
|
745
|
-
if (!sameDigests(digests(original), plan.before)
|
|
746
|
-
|| !sameRaw(original.configRaw, prepared.originalConfigRaw)) {
|
|
747
|
-
throw new Error('Claude native source changed before backup export.');
|
|
748
|
-
}
|
|
749
|
-
this.lease.assertHeld();
|
|
750
|
-
this.assertSupported();
|
|
751
|
-
return { before: Object.freeze({ ...plan.before }),
|
|
752
|
-
credentialBytes: original.credentialRaw === null ? null : Buffer.from(original.credentialRaw),
|
|
753
|
-
configBytes: original.configRaw === null ? null : Buffer.from(original.configRaw) };
|
|
754
|
-
}
|
|
755
|
-
|
|
756
725
|
public writePreparedIfCurrent(plan: IClaudeNativePreparedWrite): TClaudeNativeWriteResult {
|
|
757
726
|
this.lease.assertHeld();
|
|
758
727
|
this.assertSupported();
|
|
@@ -5,7 +5,7 @@ import type { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
|
5
5
|
export interface IAuthSwitchManagedCodexOptions {
|
|
6
6
|
/** Private runtime root; only the disposable control socket lives here. */
|
|
7
7
|
runtimeDirectory: string;
|
|
8
|
-
/** Caller-owned, durable native Codex state home, reserved to one account and scope by
|
|
8
|
+
/** Caller-owned, durable native Codex state home, reserved to one account and scope by NoSQLDB. */
|
|
9
9
|
codexHomeDirectory: string;
|
|
10
10
|
workspaceDirectory: string;
|
|
11
11
|
executable?: string;
|
package/ts/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from './watchpolicy.js';
|
|
|
26
26
|
export * from './classes.authorityclient.js';
|
|
27
27
|
export * from './classes.authoritycli.js';
|
|
28
28
|
export * from './classes.authoritydaemon.js';
|
|
29
|
+
export * from './classes.authorityimport.js';
|
|
29
30
|
export * from './classes.authorityservice.js';
|
|
30
31
|
|
|
31
32
|
import { AuthSwitchCli } from './classes.cli.js';
|
|
@@ -33,7 +34,8 @@ import { runAuthSwitchAuthorityCli } from './classes.authoritycli.js';
|
|
|
33
34
|
import { AuthSwitchAuthorityService, resolveAuthSwitchAuthorityPaths, runAuthSwitchAuthorityDaemon } from './classes.authorityservice.js';
|
|
34
35
|
|
|
35
36
|
export const runCli = async (argvArg: string[] = process.argv.slice(2)): Promise<void> => {
|
|
36
|
-
if (argvArg[0] === 'account'
|
|
37
|
+
if (argvArg[0] === 'account'
|
|
38
|
+
|| (argvArg[0] === 'authority' && (argvArg[1] === 'doctor' || argvArg[1] === 'import'))) {
|
|
37
39
|
process.exitCode = await runAuthSwitchAuthorityCli(argvArg);
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
@@ -57,7 +59,8 @@ export const runCli = async (argvArg: string[] = process.argv.slice(2)): Promise
|
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
throw new Error('Usage: authswitch authority daemon | service install|status|enable|start|stop'
|
|
62
|
+
throw new Error('Usage: authswitch authority daemon | service install|status|enable|start|stop '
|
|
63
|
+
+ '| doctor | import inventory');
|
|
61
64
|
}
|
|
62
65
|
const exitCode = await new AuthSwitchCli().run(argvArg);
|
|
63
66
|
process.exitCode = exitCode;
|
package/ts/plugins.ts
CHANGED
|
@@ -5,12 +5,11 @@ import * as path from 'node:path';
|
|
|
5
5
|
import * as childProcess from 'node:child_process';
|
|
6
6
|
import * as crypto from 'node:crypto';
|
|
7
7
|
import * as net from 'node:net';
|
|
8
|
-
import * as stream from 'node:stream';
|
|
9
8
|
import { createRequire } from 'node:module';
|
|
10
9
|
import { fileURLToPath } from 'node:url';
|
|
11
10
|
import type { DatabaseSync } from 'node:sqlite';
|
|
12
11
|
|
|
13
|
-
export { childProcess, crypto, fileURLToPath, fs, net, os, path
|
|
12
|
+
export { childProcess, crypto, fileURLToPath, fs, net, os, path };
|
|
14
13
|
export type { DatabaseSync };
|
|
15
14
|
const nativeRequire = createRequire(import.meta.url);
|
|
16
15
|
/** SQLite is needed only when accessing a Codex state database, not when hosting account APIs. */
|
|
@@ -26,17 +25,18 @@ export type TProperLockfile = typeof properLockfile;
|
|
|
26
25
|
export const loadProperLockfile = (): Promise<TProperLockfile> => import('proper-lockfile');
|
|
27
26
|
|
|
28
27
|
// @lossless.org modules
|
|
29
|
-
|
|
28
|
+
/** Account document models, sessions and exact persistence. */
|
|
29
|
+
import * as nosqldb from '@lossless.org/client/nosqldb';
|
|
30
|
+
/** The embedded engine that hosts the per-user account database behind a private Unix socket. */
|
|
31
|
+
import * as nosqldbEngine from '@lossless.org/nosqldb';
|
|
32
|
+
export { nosqldb, nosqldbEngine };
|
|
30
33
|
|
|
31
34
|
// @push.rocks modules
|
|
32
35
|
import * as smartconsole from '@push.rocks/smartconsole';
|
|
33
|
-
import * as smartcrypto from '@push.rocks/smartcrypto';
|
|
34
|
-
import * as smartdata from '@push.rocks/smartdata';
|
|
35
|
-
import * as smartdb from '@push.rocks/smartdb';
|
|
36
36
|
import * as smartsecret from '@push.rocks/smartsecret';
|
|
37
37
|
import * as smartdaemon from '@push.rocks/smartdaemon';
|
|
38
38
|
import * as typedrequest from '@api.global/typedrequest';
|
|
39
|
-
export {
|
|
39
|
+
export { smartconsole, smartsecret, smartdaemon, typedrequest };
|
|
40
40
|
|
|
41
41
|
// @modelprofile.com modules
|
|
42
42
|
import * as flexModels from '@modelprofile.com/flexharness-models';
|
|
@@ -1,33 +1,45 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { assertStoredAuthorityMeta } from '../classes.authoritymodels.js';
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
3
2
|
|
|
4
3
|
const reserved = new Set(['_id', '_smartdataRevision', '_createdAt', '_updatedAt']);
|
|
5
|
-
const
|
|
4
|
+
const currentKeys = ['id', 'epoch', 'revision', 'updateId'];
|
|
6
5
|
const uuid = (value: unknown): boolean => typeof value === 'string' && /^[a-f0-9-]{36}$/.test(value);
|
|
7
6
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Brings the metadata document of every published release to the current shape, under the exclusive
|
|
9
|
+
* LocalNoSqlDb root owner. 6.4/6.5 wrote exactly the current shape. 7.0.0 added `schemaVersion: 2` and an
|
|
10
|
+
* `admission` marker; both are removed in one compare-and-set, and an unreleased build that had already
|
|
11
|
+
* dropped `schemaVersion` loses the marker the same way. A store whose marker is not `open` was closed by a
|
|
12
|
+
* restore and is refused, never opened. Every other document is refused as unknown.
|
|
13
|
+
*
|
|
14
|
+
* The stored shape is the only record of completion: no ledger of applied module ids exists. That is what
|
|
15
|
+
* makes rewriting this published module in place sound, and it holds only while every accepted shape maps to
|
|
16
|
+
* the current one in at most one atomic write and the current shape maps to itself.
|
|
17
|
+
*/
|
|
18
|
+
export const migrateAuthorityMeta = async (db: plugins.nosqldb.SmartdataDb): Promise<void> => {
|
|
10
19
|
const collection = db.mongoDb.collection('authswitch_authority_meta');
|
|
11
20
|
const stored = await collection.findOne({ id: 'authswitch-authority' });
|
|
12
21
|
if (!stored) throw new Error('Existing authswitch authority database has no metadata.');
|
|
13
22
|
const body = Object.fromEntries(Object.entries(stored).filter(([key]) => !reserved.has(key)));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const hasSchemaVersion = Object.hasOwn(body, 'schemaVersion');
|
|
24
|
+
const hasAdmission = Object.hasOwn(body, 'admission');
|
|
25
|
+
if (Object.keys(body).length !== currentKeys.length + Number(hasSchemaVersion) + Number(hasAdmission)
|
|
26
|
+
|| !currentKeys.every(key => Object.hasOwn(body, key))
|
|
27
|
+
|| (hasSchemaVersion && (!hasAdmission || body.schemaVersion !== 2))
|
|
28
|
+
|| (hasAdmission && body.admission !== 'open' && body.admission !== 'closed_restored')
|
|
19
29
|
|| body.id !== 'authswitch-authority' || !uuid(body.epoch) || !uuid(body.updateId)
|
|
20
30
|
|| !Number.isSafeInteger(body.revision) || Number(body.revision) < 0) {
|
|
21
31
|
throw new Error('Existing authswitch authority metadata has an unknown schema.');
|
|
22
32
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
if (!hasAdmission) return;
|
|
34
|
+
if (body.admission !== 'open') {
|
|
35
|
+
throw new Error('Authswitch authority store was closed by a restore and cannot be opened; '
|
|
36
|
+
+ 'set the store aside and log in to each account again.');
|
|
37
|
+
}
|
|
38
|
+
const result = await collection.updateOne(
|
|
39
|
+
{ _id: stored._id, id: 'authswitch-authority', admission: 'open',
|
|
40
|
+
schemaVersion: hasSchemaVersion ? 2 : { $exists: false } },
|
|
41
|
+
{ $unset: hasSchemaVersion ? { schemaVersion: '', admission: '' } : { admission: '' } });
|
|
26
42
|
if (result.matchedCount !== 1 || result.modifiedCount !== 1) {
|
|
27
43
|
throw new Error('Authswitch authority metadata migration did not commit exactly once.');
|
|
28
44
|
}
|
|
29
|
-
const migrated = await collection.findOne({ _id: stored._id });
|
|
30
|
-
if (!migrated) throw new Error('Authswitch authority metadata disappeared during migration.');
|
|
31
|
-
assertStoredAuthorityMeta(Object.fromEntries(Object.entries(migrated)
|
|
32
|
-
.filter(([key]) => !reserved.has(key))));
|
|
33
45
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
const digestCollections = ['authswitch_authority_handoffs', 'authswitch_authority_migration_ledger'];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Removes what the retired backup and restore system left in the store, under the exclusive LocalNoSqlDb root
|
|
7
|
+
* owner. 6.4.0 to 7.0.0 stored a `backupDigest` on every native handoff and migration ledger row; no release
|
|
8
|
+
* ever wrote a non-null one, so a null digest is unset and a non-null one refuses the store. Only a 7.0.0
|
|
9
|
+
* restore could mark a preuse receipt `restored_history_uncertain`, and a restore never targeted a real store, so
|
|
10
|
+
* that receipt refuses the store as well. A verified or complete ledger row must name its imported account and
|
|
11
|
+
* grant; one that does not was never written by this package and is refused. Every refusal is decided before the
|
|
12
|
+
* first write, so a refused store is left exactly as it was.
|
|
13
|
+
*
|
|
14
|
+
* The stored shape is the only record of completion: no ledger of applied module ids exists. Each row is
|
|
15
|
+
* rewritten by its own compare-and-set, so an interrupted run leaves rows this module already recognises and the
|
|
16
|
+
* next start finishes them. Once migrated, a start costs one unindexed pass over the operation receipts, of the
|
|
17
|
+
* same order as the state reconciliation every start already performs, and passes over two collections that
|
|
18
|
+
* hold one row per imported source.
|
|
19
|
+
*/
|
|
20
|
+
export const removeAuthorityBackupRecords = async (db: plugins.nosqldb.SmartdataDb): Promise<void> => {
|
|
21
|
+
const operations = db.mongoDb.collection('authswitch_authority_operations');
|
|
22
|
+
if (await operations.findOne({ problem: 'restored_history_uncertain' })) {
|
|
23
|
+
throw new Error('Authswitch authority store holds a receipt written by a restore and cannot be opened; '
|
|
24
|
+
+ 'set the store aside and log in to each account again.');
|
|
25
|
+
}
|
|
26
|
+
const ledger = db.mongoDb.collection('authswitch_authority_migration_ledger');
|
|
27
|
+
if (await ledger.findOne({ status: { $in: ['verified', 'complete'] }, $or: [{ accountId: null }, { grantId: null }] })) {
|
|
28
|
+
throw new Error('Authswitch authority store holds a verified migration without its imported account and grant.');
|
|
29
|
+
}
|
|
30
|
+
const carriers = [];
|
|
31
|
+
for (const name of digestCollections) {
|
|
32
|
+
const collection = db.mongoDb.collection(name);
|
|
33
|
+
if (!await collection.findOne({ backupDigest: { $exists: true } })) continue;
|
|
34
|
+
if (await collection.findOne({ backupDigest: { $exists: true, $ne: null } })) {
|
|
35
|
+
throw new Error('Authswitch authority store holds backup proof no release could have recorded.');
|
|
36
|
+
}
|
|
37
|
+
carriers.push(collection);
|
|
38
|
+
}
|
|
39
|
+
for (const collection of carriers) {
|
|
40
|
+
const carrier = { backupDigest: { $exists: true } };
|
|
41
|
+
for (let stored = await collection.findOne(carrier); stored; stored = await collection.findOne(carrier)) {
|
|
42
|
+
const result = await collection.updateOne({ _id: stored._id, backupDigest: null },
|
|
43
|
+
{ $unset: { backupDigest: '' } });
|
|
44
|
+
if (result.matchedCount !== 1 || result.modifiedCount !== 1) {
|
|
45
|
+
throw new Error('Authswitch authority backup record removal did not commit exactly once.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
package/ts/ts_migration/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { migrateAuthorityMeta } from './0001_authority_meta.js';
|
|
3
|
+
import { removeAuthorityBackupRecords } from './0002_remove_backup_records.js';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Runs on every start of an existing store, in this fixed order. Each module recognises the published shapes it
|
|
7
|
+
* upgrades and leaves the current shape untouched, so the stored document itself records completion.
|
|
8
|
+
*/
|
|
9
|
+
export const runAuthorityMigrations = async (db: plugins.nosqldb.SmartdataDb): Promise<void> => {
|
|
10
|
+
await migrateAuthorityMeta(db);
|
|
11
|
+
await removeAuthorityBackupRecords(db);
|
|
7
12
|
};
|