@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.
Files changed (86) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +1 -3
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-import-contract.d.ts +207 -0
  5. package/dist_ts/authority-import-contract.js +18 -0
  6. package/dist_ts/classes.authoritybroker.js +1 -5
  7. package/dist_ts/classes.authoritycli.d.ts +4 -0
  8. package/dist_ts/classes.authoritycli.js +149 -12
  9. package/dist_ts/classes.authorityclient.d.ts +7 -0
  10. package/dist_ts/classes.authorityclient.js +13 -1
  11. package/dist_ts/classes.authoritydaemon.d.ts +4 -0
  12. package/dist_ts/classes.authoritydaemon.js +75 -9
  13. package/dist_ts/classes.authoritydatabase.d.ts +10 -26
  14. package/dist_ts/classes.authoritydatabase.js +68 -133
  15. package/dist_ts/classes.authorityimport.d.ts +161 -0
  16. package/dist_ts/classes.authorityimport.js +638 -0
  17. package/dist_ts/classes.authoritymodels.d.ts +39 -43
  18. package/dist_ts/classes.authoritymodels.js +209 -243
  19. package/dist_ts/classes.authoritypreuse.js +1 -2
  20. package/dist_ts/classes.authorityregistry.d.ts +2 -17
  21. package/dist_ts/classes.authorityregistry.js +18 -74
  22. package/dist_ts/classes.authoritysecrets.d.ts +1 -1
  23. package/dist_ts/classes.authoritysecrets.js +2 -2
  24. package/dist_ts/classes.authorityusage.d.ts +3 -3
  25. package/dist_ts/classes.claudeauthority.js +3 -7
  26. package/dist_ts/classes.claudenative.d.ts +1 -8
  27. package/dist_ts/classes.claudenative.js +2 -25
  28. package/dist_ts/classes.codexmanaged.d.ts +1 -1
  29. package/dist_ts/index.d.ts +1 -0
  30. package/dist_ts/index.js +6 -3
  31. package/dist_ts/plugins.d.ts +7 -7
  32. package/dist_ts/plugins.js +8 -8
  33. package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -3
  34. package/dist_ts/ts_migration/0001_authority_meta.js +29 -17
  35. package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
  36. package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
  37. package/dist_ts/ts_migration/index.d.ts +6 -3
  38. package/dist_ts/ts_migration/index.js +10 -4
  39. package/dist_ts/ts_migration/legacysources/authswitchstores.d.ts +2 -0
  40. package/dist_ts/ts_migration/legacysources/authswitchstores.js +244 -0
  41. package/dist_ts/ts_migration/legacysources/index.d.ts +18 -0
  42. package/dist_ts/ts_migration/legacysources/index.js +24 -0
  43. package/dist_ts/ts_migration/legacysources/material.d.ts +37 -0
  44. package/dist_ts/ts_migration/legacysources/material.js +120 -0
  45. package/dist_ts/ts_migration/legacysources/nativestores.d.ts +2 -0
  46. package/dist_ts/ts_migration/legacysources/nativestores.js +207 -0
  47. package/dist_ts/ts_migration/legacysources/shared.d.ts +124 -0
  48. package/dist_ts/ts_migration/legacysources/shared.js +142 -0
  49. package/package.json +6 -5
  50. package/readme.md +68 -26
  51. package/ts/00_commitinfo_data.ts +1 -1
  52. package/ts/authority-contract.ts +1 -3
  53. package/ts/authority-import-contract.ts +216 -0
  54. package/ts/classes.authoritybroker.ts +0 -4
  55. package/ts/classes.authoritycli.ts +165 -11
  56. package/ts/classes.authorityclient.ts +22 -0
  57. package/ts/classes.authoritydaemon.ts +75 -8
  58. package/ts/classes.authoritydatabase.ts +70 -145
  59. package/ts/classes.authorityimport.ts +718 -0
  60. package/ts/classes.authoritymodels.ts +253 -269
  61. package/ts/classes.authoritypreuse.ts +0 -1
  62. package/ts/classes.authorityregistry.ts +20 -91
  63. package/ts/classes.authoritysecrets.ts +1 -1
  64. package/ts/classes.authorityusage.ts +3 -3
  65. package/ts/classes.claudeauthority.ts +2 -6
  66. package/ts/classes.claudenative.ts +1 -32
  67. package/ts/classes.codexmanaged.ts +1 -1
  68. package/ts/index.ts +5 -2
  69. package/ts/plugins.ts +7 -7
  70. package/ts/ts_migration/0001_authority_meta.ts +29 -17
  71. package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
  72. package/ts/ts_migration/index.ts +10 -5
  73. package/ts/ts_migration/legacysources/authswitchstores.ts +211 -0
  74. package/ts/ts_migration/legacysources/index.ts +35 -0
  75. package/ts/ts_migration/legacysources/material.ts +134 -0
  76. package/ts/ts_migration/legacysources/nativestores.ts +204 -0
  77. package/ts/ts_migration/legacysources/shared.ts +220 -0
  78. package/dist_ts/authorityrehearsal.child.d.ts +0 -1
  79. package/dist_ts/authorityrehearsal.child.js +0 -395
  80. package/dist_ts/classes.authoritybackup.d.ts +0 -134
  81. package/dist_ts/classes.authoritybackup.js +0 -769
  82. package/dist_ts/classes.authorityrehearsal.d.ts +0 -127
  83. package/dist_ts/classes.authorityrehearsal.js +0 -632
  84. package/ts/authorityrehearsal.child.ts +0 -330
  85. package/ts/classes.authoritybackup.ts +0 -780
  86. 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, assertStoredAuthorityAccount, assertStoredAuthorityBinding,
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.smartdata.SmartdataDb): Promise<void>;
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 = <TPersisted extends { id: string },
27
- TModel extends TPersisted & plugins.smartdata.SmartDataDbDoc<TModel, TPersisted>>(
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.smartdata.SmartdataDb) => {
34
- plugins.smartdata.setDefaultManagerForDoc({ db }, model);
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, in portable model+stable-ID order. */
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, assertStoredAuthorityAccount),
62
- define('authswitch_authority_bindings', AuthSwitchAuthorityBindingModel, assertStoredAuthorityBinding),
63
- define('authswitch_authority_claude_handoffs', AuthSwitchAuthorityClaudeHandoffModel, assertStoredAuthorityClaudeHandoff),
64
- define('authswitch_authority_claude_homes', AuthSwitchAuthorityClaudeHomeModel, assertStoredAuthorityClaudeHome),
65
- define('authswitch_authority_codex_homes', AuthSwitchAuthorityCodexHomeModel, assertStoredAuthorityCodexHome),
66
- define('authswitch_authority_enrollments', AuthSwitchAuthorityEnrollmentModel, assertStoredAuthorityEnrollment),
67
- define('authswitch_authority_events', AuthSwitchAuthorityEventModel, assertStoredAuthorityEvent),
68
- define('authswitch_authority_grants', AuthSwitchAuthorityGrantModel, assertStoredAuthorityGrant),
69
- define('authswitch_authority_handoffs', AuthSwitchAuthorityHandoffModel, assertStoredAuthorityHandoff),
70
- define('authswitch_authority_meta', AuthSwitchAuthorityMetaModel, assertStoredAuthorityMeta),
71
- define('authswitch_authority_migration_ledger', AuthSwitchAuthorityMigrationLedgerModel, assertStoredAuthorityMigrationLedger),
72
- define('authswitch_authority_operations', AuthSwitchAuthorityOperationModel, assertStoredAuthorityOperationDocument),
73
- define('authswitch_authority_usage', AuthSwitchAuthorityUsageModel, assertStoredAuthorityUsage),
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 SmartData. */
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 SmartData in the authority integration. No credential belongs in this record. */
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 SmartData transaction, re-read epoch/account/login and compare every semantic
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 SmartData authority. Return null for an absent login. */
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 and backup receipt.');
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 and backup receipt.');
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 SmartData journal and both ownership transactions inside this callback. */
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 SmartData. */
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' || (argvArg[0] === 'authority' && argvArg[1] === 'doctor')) {
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, stream };
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
- import * as smartbucket from '@lossless.org/client/objectstorage';
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 { smartbucket, smartconsole, smartcrypto, smartdata, smartdb, smartsecret, smartdaemon, typedrequest };
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 type { SmartdataDb } from '@push.rocks/smartdata';
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 keys = ['id', 'epoch', 'revision', 'updateId'];
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
- /** One-time published 6.4/6.5 metadata upgrade, under the exclusive LocalSmartDb root owner. */
9
- export const migrateAuthorityMetaV1 = async (db: SmartdataDb): Promise<void> => {
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
- if (body.schemaVersion === 2) {
15
- assertStoredAuthorityMeta(body);
16
- return;
17
- }
18
- if (Object.keys(body).length !== keys.length || !keys.every(key => Object.hasOwn(body, key))
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
- const result = await collection.updateOne({ _id: stored._id, id: 'authswitch-authority',
24
- schemaVersion: { $exists: false }, admission: { $exists: false } },
25
- { $set: { schemaVersion: 2, admission: 'open' } });
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
+ };
@@ -1,7 +1,12 @@
1
- import type { SmartdataDb } from '@push.rocks/smartdata';
2
- import { migrateAuthorityMetaV1 } from './0001_authority_meta.js';
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
- /** The persisted metadata schema version records completion. Order is fixed for later migrations. */
5
- export const runAuthorityMigrations = async (db: SmartdataDb): Promise<void> => {
6
- await migrateAuthorityMetaV1(db);
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
  };