@modelprofile.com/authswitch 6.2.0 → 6.4.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 +142 -0
- package/dist_ts/authority-contract.js +3 -0
- package/dist_ts/authority-runtime-contract.d.ts +32 -0
- package/dist_ts/authority-runtime-contract.js +2 -0
- package/dist_ts/classes.authoritybroker.d.ts +73 -0
- package/dist_ts/classes.authoritybroker.js +548 -0
- package/dist_ts/classes.authorityclient.d.ts +34 -0
- package/dist_ts/classes.authorityclient.js +200 -0
- package/dist_ts/classes.authoritydaemon.d.ts +26 -0
- package/dist_ts/classes.authoritydaemon.js +249 -0
- package/dist_ts/classes.authoritydatabase.d.ts +94 -0
- package/dist_ts/classes.authoritydatabase.js +736 -0
- package/dist_ts/classes.authorityframing.d.ts +8 -0
- package/dist_ts/classes.authorityframing.js +22 -0
- package/dist_ts/classes.authoritymodels.d.ts +215 -0
- package/dist_ts/classes.authoritymodels.js +818 -0
- package/dist_ts/classes.authoritysecrets.d.ts +9 -0
- package/dist_ts/classes.authoritysecrets.js +30 -0
- package/dist_ts/classes.authorityservice.d.ts +28 -0
- package/dist_ts/classes.authorityservice.js +71 -0
- package/dist_ts/classes.cli.d.ts +56 -0
- package/dist_ts/classes.cli.js +238 -63
- package/dist_ts/classes.codexpreuse.js +2 -2
- package/dist_ts/classes.tui.js +51 -2
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +26 -1
- package/dist_ts/plugins.d.ts +9 -2
- package/dist_ts/plugins.js +10 -3
- package/dist_ts/preuse.d.ts +89 -2
- package/dist_ts/preuse.js +107 -2
- package/package.json +17 -3
- package/readme.md +118 -16
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +109 -0
- package/ts/authority-runtime-contract.ts +27 -0
- package/ts/classes.authoritybroker.ts +530 -0
- package/ts/classes.authorityclient.ts +183 -0
- package/ts/classes.authoritydaemon.ts +219 -0
- package/ts/classes.authoritydatabase.ts +725 -0
- package/ts/classes.authorityframing.ts +21 -0
- package/ts/classes.authoritymodels.ts +367 -0
- package/ts/classes.authoritysecrets.ts +27 -0
- package/ts/classes.authorityservice.ts +83 -0
- package/ts/classes.cli.ts +236 -54
- package/ts/classes.codexpreuse.ts +1 -1
- package/ts/classes.tui.ts +39 -2
- package/ts/index.ts +25 -0
- package/ts/plugins.ts +9 -2
- package/ts/preuse.ts +141 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const maxFrameBytes = 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
/** One bounded UTF-8 JSON line. Decode only after the complete byte frame arrives. */
|
|
4
|
+
export class AuthSwitchAuthorityFrameReader {
|
|
5
|
+
private readonly chunks: Buffer[] = [];
|
|
6
|
+
private bytes = 0;
|
|
7
|
+
|
|
8
|
+
public add(chunk: Buffer): string | null {
|
|
9
|
+
if (chunk.length > maxFrameBytes - this.bytes) throw new Error('Authswitch authority frame is too large.');
|
|
10
|
+
this.chunks.push(chunk);
|
|
11
|
+
this.bytes += chunk.length;
|
|
12
|
+
const end = chunk.indexOf(0x0a);
|
|
13
|
+
if (end < 0) return null;
|
|
14
|
+
if (end !== chunk.length - 1) throw new Error('Authswitch authority frame contains trailing data.');
|
|
15
|
+
const frame = Buffer.concat(this.chunks, this.bytes);
|
|
16
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(frame.subarray(0, frame.length - 1));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const authSwitchAuthorityFrameBytes = (value: string): number => Buffer.byteLength(value, 'utf8') + 1;
|
|
21
|
+
export const maxAuthSwitchAuthorityFrameBytes = maxFrameBytes;
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
export interface IStoredAuthorityMeta {
|
|
4
|
+
id: 'authswitch-authority';
|
|
5
|
+
epoch: string;
|
|
6
|
+
revision: number;
|
|
7
|
+
updateId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Identity and presentation only. No refresh grant or native-home ownership lives here. */
|
|
11
|
+
export interface IStoredAuthorityAccount {
|
|
12
|
+
id: string;
|
|
13
|
+
providerId: string;
|
|
14
|
+
issuer: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
label: string;
|
|
18
|
+
email: string | null;
|
|
19
|
+
plan: string | null;
|
|
20
|
+
primaryGrantId: string | null;
|
|
21
|
+
removed: boolean;
|
|
22
|
+
revision: number;
|
|
23
|
+
statusObservedAt: string;
|
|
24
|
+
updateId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type TStoredGrantPurpose =
|
|
28
|
+
| 'openai_managed' | 'claude_host_native' | 'claude_container_setup' | 'opencode_native';
|
|
29
|
+
|
|
30
|
+
/** Each purpose has its own grant, ownership, generation and refresh state. */
|
|
31
|
+
export interface IStoredAuthorityGrant {
|
|
32
|
+
id: string;
|
|
33
|
+
accountId: string;
|
|
34
|
+
providerId: string;
|
|
35
|
+
purpose: TStoredGrantPurpose;
|
|
36
|
+
audience: string;
|
|
37
|
+
state: 'ready' | 'exchange_may_have_been_sent' | 'retry_wait' | 'needs_reauth' | 'native' | 'legacy_native_pending' | 'removed';
|
|
38
|
+
owner: 'daemon' | 'claude_native' | 'legacy_native' | 'none';
|
|
39
|
+
grantGeneration: number;
|
|
40
|
+
authorizationGeneration: number;
|
|
41
|
+
revision: number;
|
|
42
|
+
ciphertext: string | null;
|
|
43
|
+
accessExpiresAt: string | null;
|
|
44
|
+
attemptId: string | null;
|
|
45
|
+
retryAt: string | null;
|
|
46
|
+
retryCount: number;
|
|
47
|
+
problem: 'none' | 'provider_unavailable' | 'exchange_uncertain' | 'provider_rejected' | 'native_owner';
|
|
48
|
+
statusObservedAt: string;
|
|
49
|
+
updateId: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IStoredAuthorityBinding {
|
|
53
|
+
id: string;
|
|
54
|
+
accountId: string;
|
|
55
|
+
grantId: string;
|
|
56
|
+
runtime: 'flex' | 'codex' | 'opencode' | 'claude';
|
|
57
|
+
scopeId: string;
|
|
58
|
+
incarnationId: string;
|
|
59
|
+
revision: number;
|
|
60
|
+
grantAuthorizationGeneration: number;
|
|
61
|
+
capabilityHash: string;
|
|
62
|
+
updateId: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Codex enrollment is separate from a refresh grant and sealed under its own record ID. */
|
|
66
|
+
export interface IStoredAuthorityEnrollment {
|
|
67
|
+
id: string;
|
|
68
|
+
accountId: string;
|
|
69
|
+
ciphertext: string;
|
|
70
|
+
revision: number;
|
|
71
|
+
updateId: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Journal for a native-home handoff. A prepared grant is never usable. */
|
|
75
|
+
export interface IStoredAuthorityHandoff {
|
|
76
|
+
id: string;
|
|
77
|
+
accountId: string;
|
|
78
|
+
grantId: string;
|
|
79
|
+
nativeHomeId: string;
|
|
80
|
+
direction: 'to_native' | 'to_daemon';
|
|
81
|
+
phase: 'prepared' | 'source_stopped' | 'target_written' | 'verified' | 'committed' | 'needs_reauth' | 'aborted';
|
|
82
|
+
operationId: string;
|
|
83
|
+
sourceDigest: string;
|
|
84
|
+
verifiedSourceDigest: string | null;
|
|
85
|
+
backupDigest: string | null;
|
|
86
|
+
revision: number;
|
|
87
|
+
statusObservedAt: string;
|
|
88
|
+
updateId: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** One-time legacy source inventory; source path is hashed, never exposed in snapshots. */
|
|
92
|
+
export interface IStoredAuthorityMigrationLedger {
|
|
93
|
+
id: string;
|
|
94
|
+
version: number;
|
|
95
|
+
sourceKind: 'authswitch_stash' | 'authswitch_backup' | 'agl_flex' | 'codex_native' | 'opencode_native' | 'claude_native';
|
|
96
|
+
sourcePathHash: string;
|
|
97
|
+
sourceDigest: string;
|
|
98
|
+
status: 'pending_native_owner' | 'prepared' | 'verified' | 'complete' | 'quarantined';
|
|
99
|
+
accountId: string | null;
|
|
100
|
+
grantId: string | null;
|
|
101
|
+
backupDigest: string | null;
|
|
102
|
+
revision: number;
|
|
103
|
+
statusObservedAt: string;
|
|
104
|
+
updateId: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface IStoredAuthorityEvent {
|
|
108
|
+
id: string;
|
|
109
|
+
epoch: string;
|
|
110
|
+
revision: number;
|
|
111
|
+
kind: 'account' | 'binding';
|
|
112
|
+
accountId: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const object = (value: unknown): value is Record<string, unknown> =>
|
|
116
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
117
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
118
|
+
const exactKeys = (value: Record<string, unknown>, keys: readonly string[]): boolean =>
|
|
119
|
+
Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
120
|
+
const bounded = (value: unknown, max = 512): value is string =>
|
|
121
|
+
typeof value === 'string' && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
122
|
+
const iso = (value: unknown): value is string =>
|
|
123
|
+
typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
124
|
+
const uuid = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9-]{36}$/.test(value);
|
|
125
|
+
const hash = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
126
|
+
const revision = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
127
|
+
const ciphertext = (value: unknown): boolean => value === null
|
|
128
|
+
|| (typeof value === 'string' && value.length > 0 && value.length <= 131072 && /^[A-Za-z0-9_-]+$/.test(value));
|
|
129
|
+
|
|
130
|
+
export const assertStoredAuthorityMeta: (value: unknown) => asserts value is IStoredAuthorityMeta = value => {
|
|
131
|
+
if (!object(value) || !exactKeys(value, ['id', 'epoch', 'revision', 'updateId'])
|
|
132
|
+
|| value.id !== 'authswitch-authority' || !uuid(value.epoch)
|
|
133
|
+
|| !revision(value.revision) || !uuid(value.updateId)) throw new Error('Invalid authswitch authority metadata.');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const assertStoredAuthorityAccount: (value: unknown) => asserts value is IStoredAuthorityAccount = value => {
|
|
137
|
+
if (!object(value) || !exactKeys(value, ['id', 'providerId', 'issuer', 'subject', 'workspaceId', 'label',
|
|
138
|
+
'email', 'plan', 'primaryGrantId', 'removed', 'revision', 'statusObservedAt', 'updateId'])
|
|
139
|
+
|| !hash(value.id) || typeof value.providerId !== 'string'
|
|
140
|
+
|| !/^[a-z][a-z0-9-]{0,63}$/.test(value.providerId)
|
|
141
|
+
|| !bounded(value.issuer) || !bounded(value.subject) || !bounded(value.workspaceId)
|
|
142
|
+
|| !bounded(value.label, 128) || (value.email !== null && !bounded(value.email, 320))
|
|
143
|
+
|| (value.plan !== null && !bounded(value.plan, 128))
|
|
144
|
+
|| (value.primaryGrantId !== null && !hash(value.primaryGrantId))
|
|
145
|
+
|| typeof value.removed !== 'boolean' || !revision(value.revision)
|
|
146
|
+
|| !iso(value.statusObservedAt) || !uuid(value.updateId)) throw new Error('Invalid authswitch account record.');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const assertStoredAuthorityGrant: (value: unknown) => asserts value is IStoredAuthorityGrant = value => {
|
|
150
|
+
if (!object(value) || !exactKeys(value, ['id', 'accountId', 'providerId', 'purpose', 'audience',
|
|
151
|
+
'state', 'owner', 'grantGeneration', 'authorizationGeneration', 'revision', 'ciphertext',
|
|
152
|
+
'accessExpiresAt', 'attemptId', 'retryAt', 'retryCount', 'problem', 'statusObservedAt', 'updateId'])
|
|
153
|
+
|| !hash(value.id) || !hash(value.accountId)
|
|
154
|
+
|| typeof value.providerId !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(value.providerId)
|
|
155
|
+
|| !['openai_managed', 'claude_host_native', 'claude_container_setup', 'opencode_native'].includes(String(value.purpose))
|
|
156
|
+
|| !bounded(value.audience)
|
|
157
|
+
|| !['ready', 'exchange_may_have_been_sent', 'retry_wait', 'needs_reauth', 'native', 'legacy_native_pending', 'removed'].includes(String(value.state))
|
|
158
|
+
|| !['daemon', 'claude_native', 'legacy_native', 'none'].includes(String(value.owner))
|
|
159
|
+
|| !revision(value.grantGeneration) || !revision(value.authorizationGeneration)
|
|
160
|
+
|| !revision(value.revision) || !ciphertext(value.ciphertext)
|
|
161
|
+
|| (value.accessExpiresAt !== null && !iso(value.accessExpiresAt))
|
|
162
|
+
|| (value.attemptId !== null && !uuid(value.attemptId))
|
|
163
|
+
|| (value.retryAt !== null && !iso(value.retryAt))
|
|
164
|
+
|| !revision(value.retryCount)
|
|
165
|
+
|| !['none', 'provider_unavailable', 'exchange_uncertain', 'provider_rejected', 'native_owner'].includes(String(value.problem))
|
|
166
|
+
|| !iso(value.statusObservedAt) || !uuid(value.updateId)) throw new Error('Invalid authswitch grant record.');
|
|
167
|
+
if (['ready', 'exchange_may_have_been_sent', 'retry_wait'].includes(String(value.state))
|
|
168
|
+
&& (value.owner !== 'daemon' || value.ciphertext === null)) throw new Error('An active daemon grant is missing.');
|
|
169
|
+
if ((value.state === 'exchange_may_have_been_sent') !== (value.attemptId !== null)) throw new Error('Invalid refresh attempt marker.');
|
|
170
|
+
if ((value.state === 'retry_wait') !== (value.retryAt !== null)) throw new Error('Invalid refresh retry marker.');
|
|
171
|
+
if (value.state === 'removed' && (value.ciphertext !== null || value.owner !== 'none')) throw new Error('A removed grant was retained.');
|
|
172
|
+
if (value.state === 'legacy_native_pending' && value.owner !== 'legacy_native') throw new Error('A legacy native grant cannot be daemon-owned.');
|
|
173
|
+
if (value.purpose === 'openai_managed' && value.providerId !== 'openai') throw new Error('OpenAI grant provider mismatch.');
|
|
174
|
+
if (value.purpose === 'claude_container_setup' && value.owner === 'claude_native') throw new Error('Container setup grant has a native host owner.');
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const assertStoredAuthorityBinding: (value: unknown) => asserts value is IStoredAuthorityBinding = value => {
|
|
178
|
+
if (!object(value) || !exactKeys(value, ['id', 'accountId', 'grantId', 'runtime', 'scopeId', 'incarnationId',
|
|
179
|
+
'revision', 'grantAuthorizationGeneration', 'capabilityHash', 'updateId'])
|
|
180
|
+
|| !hash(value.id) || !hash(value.accountId) || !hash(value.grantId)
|
|
181
|
+
|| !['flex', 'codex', 'opencode', 'claude'].includes(String(value.runtime))
|
|
182
|
+
|| !bounded(value.scopeId, 512) || !bounded(value.incarnationId, 256)
|
|
183
|
+
|| !revision(value.revision) || !revision(value.grantAuthorizationGeneration)
|
|
184
|
+
|| !hash(value.capabilityHash)
|
|
185
|
+
|| !uuid(value.updateId)) throw new Error('Invalid authswitch binding record.');
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const assertStoredAuthorityEnrollment: (value: unknown) => asserts value is IStoredAuthorityEnrollment = value => {
|
|
189
|
+
if (!object(value) || !exactKeys(value, ['id', 'accountId', 'ciphertext', 'revision', 'updateId'])
|
|
190
|
+
|| !hash(value.id) || !hash(value.accountId) || !ciphertext(value.ciphertext) || value.ciphertext === null
|
|
191
|
+
|| !revision(value.revision) || !uuid(value.updateId)) throw new Error('Invalid authswitch enrollment record.');
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const assertStoredAuthorityHandoff: (value: unknown) => asserts value is IStoredAuthorityHandoff = value => {
|
|
195
|
+
if (!object(value) || !exactKeys(value, ['id', 'accountId', 'grantId', 'nativeHomeId', 'direction', 'phase',
|
|
196
|
+
'operationId', 'sourceDigest', 'verifiedSourceDigest', 'backupDigest', 'revision', 'statusObservedAt', 'updateId'])
|
|
197
|
+
|| !hash(value.id) || !hash(value.accountId) || !hash(value.grantId) || !hash(value.nativeHomeId)
|
|
198
|
+
|| !['to_native', 'to_daemon'].includes(String(value.direction))
|
|
199
|
+
|| !['prepared', 'source_stopped', 'target_written', 'verified', 'committed', 'needs_reauth', 'aborted'].includes(String(value.phase))
|
|
200
|
+
|| !uuid(value.operationId) || !hash(value.sourceDigest)
|
|
201
|
+
|| (value.verifiedSourceDigest !== null && !hash(value.verifiedSourceDigest))
|
|
202
|
+
|| (value.backupDigest !== null && !hash(value.backupDigest))
|
|
203
|
+
|| !revision(value.revision) || !iso(value.statusObservedAt) || !uuid(value.updateId)) {
|
|
204
|
+
throw new Error('Invalid authswitch handoff record.');
|
|
205
|
+
}
|
|
206
|
+
if (['verified', 'committed'].includes(String(value.phase))
|
|
207
|
+
&& (value.backupDigest === null || value.verifiedSourceDigest === null)) {
|
|
208
|
+
throw new Error('A verified native handoff requires source and backup proof.');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const assertStoredAuthorityMigrationLedger: (value: unknown) => asserts value is IStoredAuthorityMigrationLedger = value => {
|
|
213
|
+
if (!object(value) || !exactKeys(value, ['id', 'version', 'sourceKind', 'sourcePathHash', 'sourceDigest',
|
|
214
|
+
'status', 'accountId', 'grantId', 'backupDigest', 'revision', 'statusObservedAt', 'updateId'])
|
|
215
|
+
|| !hash(value.id) || !revision(value.version) || value.version === 0
|
|
216
|
+
|| !['authswitch_stash', 'authswitch_backup', 'agl_flex', 'codex_native', 'opencode_native', 'claude_native'].includes(String(value.sourceKind))
|
|
217
|
+
|| !hash(value.sourcePathHash) || !hash(value.sourceDigest)
|
|
218
|
+
|| !['pending_native_owner', 'prepared', 'verified', 'complete', 'quarantined'].includes(String(value.status))
|
|
219
|
+
|| (value.accountId !== null && !hash(value.accountId))
|
|
220
|
+
|| (value.grantId !== null && !hash(value.grantId))
|
|
221
|
+
|| (value.backupDigest !== null && !hash(value.backupDigest))
|
|
222
|
+
|| !revision(value.revision) || !iso(value.statusObservedAt) || !uuid(value.updateId)) {
|
|
223
|
+
throw new Error('Invalid authswitch migration ledger record.');
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const assertStoredAuthorityEvent: (value: unknown) => asserts value is IStoredAuthorityEvent = value => {
|
|
228
|
+
if (!object(value) || !exactKeys(value, ['id', 'epoch', 'revision', 'kind', 'accountId'])
|
|
229
|
+
|| !uuid(value.id) || !uuid(value.epoch) || !revision(value.revision)
|
|
230
|
+
|| !['account', 'binding'].includes(String(value.kind)) || !hash(value.accountId)) throw new Error('Invalid authswitch event record.');
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_meta' })
|
|
234
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityMeta })
|
|
235
|
+
export class AuthSwitchAuthorityMetaModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityMetaModel, IStoredAuthorityMeta> {
|
|
236
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityMetaModel>;
|
|
237
|
+
@plugins.smartdata.unI() public id!: 'authswitch-authority';
|
|
238
|
+
@plugins.smartdata.svDb() public epoch!: string;
|
|
239
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
240
|
+
@plugins.smartdata.svDb() public updateId!: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_account_provider_id', key: { providerId: 1, id: 1 } })
|
|
244
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_accounts' })
|
|
245
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityAccount })
|
|
246
|
+
export class AuthSwitchAuthorityAccountModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityAccountModel, IStoredAuthorityAccount> {
|
|
247
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityAccountModel>;
|
|
248
|
+
@plugins.smartdata.unI() public id!: string;
|
|
249
|
+
@plugins.smartdata.svDb() public providerId!: string;
|
|
250
|
+
@plugins.smartdata.svDb() public issuer!: string;
|
|
251
|
+
@plugins.smartdata.svDb() public subject!: string;
|
|
252
|
+
@plugins.smartdata.svDb() public workspaceId!: string;
|
|
253
|
+
@plugins.smartdata.svDb() public label!: string;
|
|
254
|
+
@plugins.smartdata.svDb() public email!: string | null;
|
|
255
|
+
@plugins.smartdata.svDb() public plan!: string | null;
|
|
256
|
+
@plugins.smartdata.svDb() public primaryGrantId!: string | null;
|
|
257
|
+
@plugins.smartdata.svDb() public removed!: boolean;
|
|
258
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
259
|
+
@plugins.smartdata.svDb() public statusObservedAt!: string;
|
|
260
|
+
@plugins.smartdata.svDb() public updateId!: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_grant_state_id', key: { state: 1, id: 1 } })
|
|
264
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_grant_account_id', key: { accountId: 1, id: 1 } })
|
|
265
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_grants' })
|
|
266
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityGrant })
|
|
267
|
+
export class AuthSwitchAuthorityGrantModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityGrantModel, IStoredAuthorityGrant> {
|
|
268
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityGrantModel>;
|
|
269
|
+
@plugins.smartdata.unI() public id!: string;
|
|
270
|
+
@plugins.smartdata.svDb() public accountId!: string;
|
|
271
|
+
@plugins.smartdata.svDb() public providerId!: string;
|
|
272
|
+
@plugins.smartdata.svDb() public purpose!: TStoredGrantPurpose;
|
|
273
|
+
@plugins.smartdata.svDb() public audience!: string;
|
|
274
|
+
@plugins.smartdata.svDb() public state!: IStoredAuthorityGrant['state'];
|
|
275
|
+
@plugins.smartdata.svDb() public owner!: IStoredAuthorityGrant['owner'];
|
|
276
|
+
@plugins.smartdata.svDb() public grantGeneration!: number;
|
|
277
|
+
@plugins.smartdata.svDb() public authorizationGeneration!: number;
|
|
278
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
279
|
+
@plugins.smartdata.svDb() public ciphertext!: string | null;
|
|
280
|
+
@plugins.smartdata.svDb() public accessExpiresAt!: string | null;
|
|
281
|
+
@plugins.smartdata.svDb() public attemptId!: string | null;
|
|
282
|
+
@plugins.smartdata.svDb() public retryAt!: string | null;
|
|
283
|
+
@plugins.smartdata.svDb() public retryCount!: number;
|
|
284
|
+
@plugins.smartdata.svDb() public problem!: IStoredAuthorityGrant['problem'];
|
|
285
|
+
@plugins.smartdata.svDb() public statusObservedAt!: string;
|
|
286
|
+
@plugins.smartdata.svDb() public updateId!: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_binding_account_id', key: { accountId: 1, id: 1 } })
|
|
290
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_bindings' })
|
|
291
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityBinding })
|
|
292
|
+
export class AuthSwitchAuthorityBindingModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityBindingModel, IStoredAuthorityBinding> {
|
|
293
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityBindingModel>;
|
|
294
|
+
@plugins.smartdata.unI() public id!: string;
|
|
295
|
+
@plugins.smartdata.svDb() public accountId!: string;
|
|
296
|
+
@plugins.smartdata.svDb() public grantId!: string;
|
|
297
|
+
@plugins.smartdata.svDb() public runtime!: IStoredAuthorityBinding['runtime'];
|
|
298
|
+
@plugins.smartdata.svDb() public scopeId!: string;
|
|
299
|
+
@plugins.smartdata.svDb() public incarnationId!: string;
|
|
300
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
301
|
+
@plugins.smartdata.svDb() public grantAuthorizationGeneration!: number;
|
|
302
|
+
@plugins.smartdata.svDb() public capabilityHash!: string;
|
|
303
|
+
@plugins.smartdata.svDb() public updateId!: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_enrollment_account_id', key: { accountId: 1, id: 1 } })
|
|
307
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_enrollments' })
|
|
308
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityEnrollment })
|
|
309
|
+
export class AuthSwitchAuthorityEnrollmentModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityEnrollmentModel, IStoredAuthorityEnrollment> {
|
|
310
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityEnrollmentModel>;
|
|
311
|
+
@plugins.smartdata.unI() public id!: string;
|
|
312
|
+
@plugins.smartdata.svDb() public accountId!: string;
|
|
313
|
+
@plugins.smartdata.svDb() public ciphertext!: string;
|
|
314
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
315
|
+
@plugins.smartdata.svDb() public updateId!: string;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_handoff_grant_id', key: { grantId: 1, id: 1 } })
|
|
319
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_handoffs' })
|
|
320
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityHandoff })
|
|
321
|
+
export class AuthSwitchAuthorityHandoffModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityHandoffModel, IStoredAuthorityHandoff> {
|
|
322
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityHandoffModel>;
|
|
323
|
+
@plugins.smartdata.unI() public id!: string;
|
|
324
|
+
@plugins.smartdata.svDb() public accountId!: string;
|
|
325
|
+
@plugins.smartdata.svDb() public grantId!: string;
|
|
326
|
+
@plugins.smartdata.svDb() public nativeHomeId!: string;
|
|
327
|
+
@plugins.smartdata.svDb() public direction!: IStoredAuthorityHandoff['direction'];
|
|
328
|
+
@plugins.smartdata.svDb() public phase!: IStoredAuthorityHandoff['phase'];
|
|
329
|
+
@plugins.smartdata.svDb() public operationId!: string;
|
|
330
|
+
@plugins.smartdata.svDb() public sourceDigest!: string;
|
|
331
|
+
@plugins.smartdata.svDb() public verifiedSourceDigest!: string | null;
|
|
332
|
+
@plugins.smartdata.svDb() public backupDigest!: string | null;
|
|
333
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
334
|
+
@plugins.smartdata.svDb() public statusObservedAt!: string;
|
|
335
|
+
@plugins.smartdata.svDb() public updateId!: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_migration_status_id', key: { status: 1, id: 1 } })
|
|
339
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_migration_ledger' })
|
|
340
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityMigrationLedger })
|
|
341
|
+
export class AuthSwitchAuthorityMigrationLedgerModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityMigrationLedgerModel, IStoredAuthorityMigrationLedger> {
|
|
342
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityMigrationLedgerModel>;
|
|
343
|
+
@plugins.smartdata.unI() public id!: string;
|
|
344
|
+
@plugins.smartdata.svDb() public version!: number;
|
|
345
|
+
@plugins.smartdata.svDb() public sourceKind!: IStoredAuthorityMigrationLedger['sourceKind'];
|
|
346
|
+
@plugins.smartdata.svDb() public sourcePathHash!: string;
|
|
347
|
+
@plugins.smartdata.svDb() public sourceDigest!: string;
|
|
348
|
+
@plugins.smartdata.svDb() public status!: IStoredAuthorityMigrationLedger['status'];
|
|
349
|
+
@plugins.smartdata.svDb() public accountId!: string | null;
|
|
350
|
+
@plugins.smartdata.svDb() public grantId!: string | null;
|
|
351
|
+
@plugins.smartdata.svDb() public backupDigest!: string | null;
|
|
352
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
353
|
+
@plugins.smartdata.svDb() public statusObservedAt!: string;
|
|
354
|
+
@plugins.smartdata.svDb() public updateId!: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
@plugins.smartdata.compoundIndex({ name: 'authority_event_revision_id', key: { revision: 1, id: 1 } })
|
|
358
|
+
@plugins.smartdata.managed({ collectionName: 'authswitch_authority_events' })
|
|
359
|
+
@plugins.smartdata.exactPersistence({ assertDocument: assertStoredAuthorityEvent })
|
|
360
|
+
export class AuthSwitchAuthorityEventModel extends plugins.smartdata.SmartDataDbDoc<AuthSwitchAuthorityEventModel, IStoredAuthorityEvent> {
|
|
361
|
+
declare static exact: plugins.smartdata.TExact<AuthSwitchAuthorityEventModel>;
|
|
362
|
+
@plugins.smartdata.unI() public id!: string;
|
|
363
|
+
@plugins.smartdata.svDb() public epoch!: string;
|
|
364
|
+
@plugins.smartdata.svDb() public revision!: number;
|
|
365
|
+
@plugins.smartdata.svDb() public kind!: IStoredAuthorityEvent['kind'];
|
|
366
|
+
@plugins.smartdata.svDb() public accountId!: string;
|
|
367
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
export interface IAuthSwitchSecretCodec {
|
|
4
|
+
seal(accountId: string, plaintext: Uint8Array): Promise<string>;
|
|
5
|
+
unseal(accountId: string, ciphertext: string): Promise<Uint8Array>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const secretName = (accountId: string): string => {
|
|
9
|
+
if (!/^[a-f0-9]{64}$/.test(accountId)) throw new Error('Invalid account identity for secret custody.');
|
|
10
|
+
return `authswitch.account.${accountId}`;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** TPM-sealed byte payloads live only as ciphertext in SmartData. */
|
|
14
|
+
export class AuthSwitchTpmSecretCodec implements IAuthSwitchSecretCodec {
|
|
15
|
+
public async seal(accountId: string, plaintext: Uint8Array): Promise<string> {
|
|
16
|
+
const ciphertext = await plugins.smartsecret.sealSmartSecretTpm2Credential(secretName(accountId), plaintext);
|
|
17
|
+
try { return Buffer.from(ciphertext).toString('base64url'); }
|
|
18
|
+
finally { ciphertext.fill(0); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async unseal(accountId: string, ciphertext: string): Promise<Uint8Array> {
|
|
22
|
+
if (!/^[A-Za-z0-9_-]{1,131072}$/.test(ciphertext)) throw new Error('Invalid sealed account credential.');
|
|
23
|
+
const bytes = Buffer.from(ciphertext, 'base64url');
|
|
24
|
+
try { return await plugins.smartsecret.unsealSmartSecretTpm2Credential(secretName(accountId), bytes); }
|
|
25
|
+
finally { bytes.fill(0); }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { AuthSwitchAuthorityDaemon } from './classes.authoritydaemon.js';
|
|
3
|
+
|
|
4
|
+
export interface IAuthSwitchAuthorityPaths {
|
|
5
|
+
runtimeDirectory: string;
|
|
6
|
+
dataDirectory: string;
|
|
7
|
+
databaseSocketPath: string;
|
|
8
|
+
authoritySocketPath: string;
|
|
9
|
+
runtimeSocketPath: string;
|
|
10
|
+
runtimeSocketDirectory: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Canonical per-user paths. An explicit runtime directory is also usable by a trusted container host. */
|
|
14
|
+
export const resolveAuthSwitchAuthorityPaths = (runtimeDirectory = process.env.XDG_RUNTIME_DIR): IAuthSwitchAuthorityPaths => {
|
|
15
|
+
if (!runtimeDirectory || !plugins.path.isAbsolute(runtimeDirectory)) {
|
|
16
|
+
throw new Error('A private XDG_RUNTIME_DIR is required for authswitch authority.');
|
|
17
|
+
}
|
|
18
|
+
const home = plugins.os.userInfo().homedir;
|
|
19
|
+
const dataHome = process.env.XDG_DATA_HOME || plugins.path.join(home, '.local/share');
|
|
20
|
+
if (!plugins.path.isAbsolute(dataHome)) throw new Error('XDG_DATA_HOME must be an absolute path.');
|
|
21
|
+
const socketDirectory = plugins.path.join(runtimeDirectory, 'authswitch');
|
|
22
|
+
const runtimeSocketDirectory = plugins.path.join(socketDirectory, 'runtime');
|
|
23
|
+
return {
|
|
24
|
+
runtimeDirectory,
|
|
25
|
+
dataDirectory: plugins.path.join(dataHome, 'authswitch', 'authority'),
|
|
26
|
+
databaseSocketPath: plugins.path.join(socketDirectory, 'internal', 'db.sock'),
|
|
27
|
+
authoritySocketPath: plugins.path.join(socketDirectory, 'management', 'authority.sock'),
|
|
28
|
+
runtimeSocketPath: plugins.path.join(runtimeSocketDirectory, 'runtime.sock'),
|
|
29
|
+
runtimeSocketDirectory,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Explicit installer and lifecycle operations for a user-scoped systemd service. */
|
|
34
|
+
export class AuthSwitchAuthorityService {
|
|
35
|
+
public readonly unit: plugins.smartdaemon.SystemdUnit;
|
|
36
|
+
public readonly unitFile: plugins.smartdaemon.SystemdUnitFile;
|
|
37
|
+
public readonly definition: plugins.smartdaemon.SystemdServiceDefinition;
|
|
38
|
+
|
|
39
|
+
constructor(runtimeDirectory: string, unitDirectory?: string) {
|
|
40
|
+
const packageRoot = plugins.path.dirname(plugins.path.dirname(plugins.fileURLToPath(import.meta.url)));
|
|
41
|
+
const options = { unitName: 'authswitch-authority.service', scope: 'user' as const, runtimeDirectory };
|
|
42
|
+
this.unit = new plugins.smartdaemon.SystemdUnit(options);
|
|
43
|
+
this.unitFile = new plugins.smartdaemon.SystemdUnitFile({ ...options, unitDirectory });
|
|
44
|
+
this.definition = new plugins.smartdaemon.SystemdServiceDefinition({
|
|
45
|
+
unitName: options.unitName, scope: 'user', description: 'Authswitch account authority', executable: process.execPath,
|
|
46
|
+
args: [plugins.path.join(packageRoot, 'cli.js'), 'authority', 'daemon'],
|
|
47
|
+
workingDirectory: packageRoot, restart: 'on-failure', killMode: 'mixed', timeoutStopSeconds: 60,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async install(): Promise<Readonly<{ changed: boolean; masked: boolean; sha256: string }>> {
|
|
52
|
+
const previous = await this.unitFile.inspect();
|
|
53
|
+
const result = await this.unitFile.install(this.definition, previous?.sha256 ?? null);
|
|
54
|
+
return { changed: result.changed, masked: result.masked, sha256: result.record.sha256 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public inspect(): Promise<plugins.smartdaemon.ISystemdUnitState> { return this.unit.inspect(); }
|
|
58
|
+
public enable(): Promise<plugins.smartdaemon.ISystemdUnitState> { return this.unit.enable(); }
|
|
59
|
+
public start(): Promise<plugins.smartdaemon.ISystemdUnitState> { return this.unit.start(); }
|
|
60
|
+
public stop(): Promise<plugins.smartdaemon.ISystemdUnitState> { return this.unit.stop(); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const runAuthSwitchAuthorityDaemon = async (paths = resolveAuthSwitchAuthorityPaths()): Promise<void> => {
|
|
64
|
+
const daemon = new AuthSwitchAuthorityDaemon({
|
|
65
|
+
dataDirectory: paths.dataDirectory,
|
|
66
|
+
socketPath: paths.databaseSocketPath,
|
|
67
|
+
authoritySocketPath: paths.authoritySocketPath,
|
|
68
|
+
runtimeSocketPath: paths.runtimeSocketPath,
|
|
69
|
+
});
|
|
70
|
+
await daemon.start();
|
|
71
|
+
await new Promise<void>((resolve, reject) => {
|
|
72
|
+
let stopping = false;
|
|
73
|
+
const stop = () => {
|
|
74
|
+
if (stopping) return;
|
|
75
|
+
stopping = true;
|
|
76
|
+
process.off('SIGINT', stop);
|
|
77
|
+
process.off('SIGTERM', stop);
|
|
78
|
+
void daemon.close().then(resolve, reject);
|
|
79
|
+
};
|
|
80
|
+
process.on('SIGINT', stop);
|
|
81
|
+
process.on('SIGTERM', stop);
|
|
82
|
+
});
|
|
83
|
+
};
|