@agent-network-protocol/dsh-anp-identity 0.1.2 → 0.1.3-rc.1
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/BOUNDARY.md +14 -2
- package/README.md +37 -4
- package/README.zh.md +10 -2
- package/V7-MANAGEMENT.md +90 -0
- package/cordis.patch.yml +4 -0
- package/lib/authorization-types.d.ts +150 -0
- package/lib/authorization-types.d.ts.map +1 -0
- package/lib/authorization-types.js +2 -0
- package/lib/authorization-types.js.map +1 -0
- package/lib/authorization.d.ts +45 -0
- package/lib/authorization.d.ts.map +1 -0
- package/lib/authorization.js +718 -0
- package/lib/authorization.js.map +1 -0
- package/lib/catalog.d.ts +7 -2
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +45 -23
- package/lib/catalog.js.map +1 -1
- package/lib/client/controller.d.ts +78 -0
- package/lib/client/controller.d.ts.map +1 -0
- package/lib/client/controller.js +365 -0
- package/lib/client/controller.js.map +1 -0
- package/lib/client/identity-icon.d.ts +3 -0
- package/lib/client/identity-icon.d.ts.map +1 -0
- package/lib/client/identity-icon.js +6 -0
- package/lib/client/identity-icon.js.map +1 -0
- package/lib/client/index.d.ts +7 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +51 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/settings-icon-slot.d.ts +13 -0
- package/lib/client/settings-icon-slot.d.ts.map +1 -0
- package/lib/client/settings-icon-slot.js +2 -0
- package/lib/client/settings-icon-slot.js.map +1 -0
- package/lib/client/styles.d.ts +4 -0
- package/lib/client/styles.d.ts.map +1 -0
- package/lib/client/styles.js +39 -0
- package/lib/client/styles.js.map +1 -0
- package/lib/client/views.d.ts +11 -0
- package/lib/client/views.d.ts.map +1 -0
- package/lib/client/views.js +287 -0
- package/lib/client/views.js.map +1 -0
- package/lib/client.js +21008 -0
- package/lib/client.js.map +7 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +413 -58
- package/lib/index.js.map +1 -1
- package/lib/management-remote.d.ts +23 -0
- package/lib/management-remote.d.ts.map +1 -0
- package/lib/management-remote.js +126 -0
- package/lib/management-remote.js.map +1 -0
- package/lib/management-types.d.ts +50 -0
- package/lib/management-types.d.ts.map +1 -0
- package/lib/management-types.js +2 -0
- package/lib/management-types.js.map +1 -0
- package/lib/provider-api.d.ts +3 -0
- package/lib/provider-api.d.ts.map +1 -1
- package/lib/provider-api.js.map +1 -1
- package/lib/remote.d.ts +4 -0
- package/lib/remote.d.ts.map +1 -0
- package/lib/remote.js +86 -0
- package/lib/remote.js.map +1 -0
- package/lib/user-client.d.ts +41 -0
- package/lib/user-client.d.ts.map +1 -0
- package/lib/user-client.js +6 -0
- package/lib/user-client.js.map +1 -0
- package/lib/user-operation.d.ts +7 -0
- package/lib/user-operation.d.ts.map +1 -0
- package/lib/user-operation.js +87 -0
- package/lib/user-operation.js.map +1 -0
- package/lib/user-service.d.ts +26 -0
- package/lib/user-service.d.ts.map +1 -0
- package/lib/user-service.js +140 -0
- package/lib/user-service.js.map +1 -0
- package/package.json +113 -10
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import lockfile from 'proper-lockfile';
|
|
5
|
+
import { normalizeHandle } from './catalog.js';
|
|
6
|
+
export * from './authorization-types.js';
|
|
7
|
+
const SCHEMA = 'anp-identity-authorization/1';
|
|
8
|
+
const ORDINARY = ['identity:read', 'identity:sign', 'identity:http-auth'];
|
|
9
|
+
const FIVE_MINUTES = 300_000;
|
|
10
|
+
const SEVEN_DAYS = 7 * 24 * 60 * 60_000;
|
|
11
|
+
export class AuthorizationError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super(code);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'AuthorizationError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function fail(code) { throw new AuthorizationError(code); }
|
|
20
|
+
function copy(value) { return structuredClone(value); }
|
|
21
|
+
function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
|
|
22
|
+
function canonical(value) {
|
|
23
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
if (value instanceof Uint8Array)
|
|
28
|
+
return canonical({ bytes: Buffer.from(value).toString('base64') });
|
|
29
|
+
if (Array.isArray(value))
|
|
30
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
31
|
+
if (typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
|
|
32
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(',')}}`;
|
|
33
|
+
}
|
|
34
|
+
return fail('invalid_operation_payload');
|
|
35
|
+
}
|
|
36
|
+
function text(value, max = 1000) {
|
|
37
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/u.test(value))
|
|
38
|
+
fail('invalid_authorization_input');
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function reference(value) {
|
|
42
|
+
return { storeId: text(value.storeId, 512), identityId: text(value.identityId, 512), did: text(value.did, 4096) };
|
|
43
|
+
}
|
|
44
|
+
function equalIdentity(a, b) {
|
|
45
|
+
return a.storeId === b.storeId && a.identityId === b.identityId && a.did === b.did;
|
|
46
|
+
}
|
|
47
|
+
function caller(value) {
|
|
48
|
+
return { consumer: text(value.consumer, 256), displayName: text(value.displayName, 256) };
|
|
49
|
+
}
|
|
50
|
+
function operation(value) {
|
|
51
|
+
if (!ORDINARY.includes(value.capability) || !/^[a-f0-9]{64}$/u.test(value.fingerprint))
|
|
52
|
+
fail('invalid_operation');
|
|
53
|
+
const result = {
|
|
54
|
+
capability: value.capability, fingerprint: value.fingerprint, summary: text(value.summary, 1024),
|
|
55
|
+
...(value.signingPurpose === undefined ? {} : { signingPurpose: text(value.signingPurpose, 256) }),
|
|
56
|
+
...(value.httpOrigin === undefined ? {} : { httpOrigin: origin(value.httpOrigin) }),
|
|
57
|
+
};
|
|
58
|
+
if ((result.capability === 'identity:sign') !== (result.signingPurpose !== undefined)
|
|
59
|
+
|| (result.capability === 'identity:http-auth') !== (result.httpOrigin !== undefined))
|
|
60
|
+
fail('invalid_operation');
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function origin(value) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = new URL(value);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return fail('invalid_http_origin');
|
|
70
|
+
}
|
|
71
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.origin !== value)
|
|
72
|
+
fail('invalid_http_origin');
|
|
73
|
+
return parsed.origin;
|
|
74
|
+
}
|
|
75
|
+
function snapshot(value) {
|
|
76
|
+
if (value.capabilities.length !== ORDINARY.length || ORDINARY.some(cap => !value.capabilities.includes(cap)))
|
|
77
|
+
fail('incompatible_identity_capabilities');
|
|
78
|
+
return {
|
|
79
|
+
version: text(value.version, 256), capabilities: [...ORDINARY],
|
|
80
|
+
signingPurposes: [...new Set(value.signingPurposes.map(v => text(v, 256)))].sort(),
|
|
81
|
+
httpOrigins: [...new Set(value.httpOrigins.map(origin))].sort(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function permits(policy, value) {
|
|
85
|
+
return policy.capabilities.includes(value.capability)
|
|
86
|
+
&& (value.signingPurpose === undefined || policy.signingPurposes.includes(value.signingPurpose))
|
|
87
|
+
&& (value.httpOrigin === undefined || policy.httpOrigins.includes(value.httpOrigin));
|
|
88
|
+
}
|
|
89
|
+
/** Compute at the Host boundary from the actual immutable operation, not a plugin-provided digest. */
|
|
90
|
+
export function freezeOperation(descriptor, canonicalPayload) {
|
|
91
|
+
return operation({ ...descriptor, fingerprint: digest({ descriptor, payload: canonicalPayload }) });
|
|
92
|
+
}
|
|
93
|
+
/** Persistent non-secret authorization ledger. Caller authentication belongs to the Host. */
|
|
94
|
+
export class AuthorizationEngine {
|
|
95
|
+
stateRoot;
|
|
96
|
+
options;
|
|
97
|
+
statePath;
|
|
98
|
+
#lockPath;
|
|
99
|
+
#now;
|
|
100
|
+
#listeners = new Set();
|
|
101
|
+
constructor(stateRoot, options) {
|
|
102
|
+
this.stateRoot = stateRoot;
|
|
103
|
+
this.options = options;
|
|
104
|
+
this.statePath = join(stateRoot, 'authorization-v1.json');
|
|
105
|
+
this.#lockPath = join(stateRoot, 'authorization-v1.lock');
|
|
106
|
+
this.#now = options.now ?? Date.now;
|
|
107
|
+
for (const ttl of [options.requestTtlMs, options.onceTtlMs, options.leaseTtlMs]) {
|
|
108
|
+
if (ttl !== undefined && (!Number.isSafeInteger(ttl) || ttl <= 0))
|
|
109
|
+
fail('invalid_authorization_ttl');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async initialize() {
|
|
113
|
+
await mkdir(this.stateRoot, { recursive: true, mode: 0o700 });
|
|
114
|
+
await this.#locked(async () => {
|
|
115
|
+
try {
|
|
116
|
+
await this.#read();
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code !== 'ENOENT')
|
|
120
|
+
throw error;
|
|
121
|
+
await this.#write({ schema: SCHEMA, generation: 0, requests: [], grants: [], executions: [], deletedIdentities: [], deletingIdentities: [], suppressions: [] });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
subscribeRequestChanges(listener) {
|
|
126
|
+
this.#listeners.add(listener);
|
|
127
|
+
return () => { this.#listeners.delete(listener); };
|
|
128
|
+
}
|
|
129
|
+
async requestCreate(verifiedCaller, input) {
|
|
130
|
+
const source = caller(verifiedCaller);
|
|
131
|
+
const parameters = { label: text(input.parameters.label, 256), domain: text(input.parameters.domain, 253), path: text(input.parameters.path, 2048),
|
|
132
|
+
...(input.parameters.handle === undefined ? {} : { handle: normalizeHandle(text(input.parameters.handle, 128)) }) };
|
|
133
|
+
const domain = new URL(`https://${parameters.domain}`);
|
|
134
|
+
if (domain.host !== parameters.domain || domain.username || domain.password || domain.pathname !== '/' || !parameters.path.startsWith('/') || /[?#]/u.test(parameters.path))
|
|
135
|
+
fail('invalid_create_parameters');
|
|
136
|
+
return this.#transaction(async (state) => {
|
|
137
|
+
await this.options.validateCaller(source);
|
|
138
|
+
return this.#request(state, source, input.requestId, input.purpose, { kind: 'create', parameters });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async requestAccess(verifiedCaller, input) {
|
|
142
|
+
const source = caller(verifiedCaller);
|
|
143
|
+
const details = {
|
|
144
|
+
kind: 'access',
|
|
145
|
+
...(input.identity === undefined ? {} : { identity: reference(input.identity) }),
|
|
146
|
+
...(input.operation === undefined ? {} : { operation: operation(input.operation) }),
|
|
147
|
+
};
|
|
148
|
+
return this.#snapshotTransaction(async (state) => {
|
|
149
|
+
await this.options.validateCaller(source);
|
|
150
|
+
if (details.identity !== undefined) {
|
|
151
|
+
this.#identityAvailable(state, details.identity);
|
|
152
|
+
const policy = snapshot(await this.options.resolveSnapshot(source, details.identity));
|
|
153
|
+
if (details.operation !== undefined && !permits(policy, details.operation))
|
|
154
|
+
fail('operation_not_permitted');
|
|
155
|
+
}
|
|
156
|
+
return this.#request(state, source, input.requestId, input.purpose, details);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
#request(state, source, id, purpose, details) {
|
|
160
|
+
text(id, 256);
|
|
161
|
+
text(purpose, 1000);
|
|
162
|
+
const fingerprint = digest({ consumer: source.consumer, purpose, ...details });
|
|
163
|
+
const existingId = state.requests.find(req => req.id === id);
|
|
164
|
+
if (existingId !== undefined) {
|
|
165
|
+
if (existingId.caller.consumer !== source.consumer || existingId.fingerprint !== fingerprint)
|
|
166
|
+
fail('request_conflict');
|
|
167
|
+
return existingId;
|
|
168
|
+
}
|
|
169
|
+
const duplicate = state.requests.find(req => req.fingerprint === fingerprint && req.status === 'pending');
|
|
170
|
+
if (duplicate !== undefined)
|
|
171
|
+
return duplicate;
|
|
172
|
+
// Changing IDs, wording, or create parameters cannot bypass a user refusal.
|
|
173
|
+
if (state.suppressions.some(block => block.consumer === source.consumer && block.kind === details.kind
|
|
174
|
+
&& (block.identity === undefined || details.kind === 'create' || details.identity === undefined || equalIdentity(block.identity, details.identity))))
|
|
175
|
+
fail('request_previously_denied');
|
|
176
|
+
const request = {
|
|
177
|
+
id, caller: source, purpose, fingerprint, version: 1, status: 'pending', prompted: false,
|
|
178
|
+
createdAt: this.#now(), expiresAt: this.#now() + (this.options.requestTtlMs ?? SEVEN_DAYS), ...details,
|
|
179
|
+
};
|
|
180
|
+
state.requests.push(request);
|
|
181
|
+
return request;
|
|
182
|
+
}
|
|
183
|
+
async getRequest(verifiedCaller, requestId) {
|
|
184
|
+
return this.#transaction(async (state) => {
|
|
185
|
+
const req = this.#findRequest(state, requestId);
|
|
186
|
+
this.#owns(verifiedCaller, req.caller.consumer);
|
|
187
|
+
return req;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async listRequests() {
|
|
191
|
+
return this.#transaction(async (state) => [...state.requests].sort((a, b) => b.createdAt - a.createdAt));
|
|
192
|
+
}
|
|
193
|
+
async markPrompted(requestId, expectedVersion) {
|
|
194
|
+
await this.#transaction(async (state) => {
|
|
195
|
+
const req = this.#findRequest(state, requestId);
|
|
196
|
+
if (expectedVersion !== undefined && req.version !== expectedVersion)
|
|
197
|
+
fail('request_conflict');
|
|
198
|
+
this.#replaceRequest(state, { ...req, prompted: true });
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async cancel(verifiedCaller, requestId, expectedVersion) {
|
|
202
|
+
return this.#transaction(async (state) => {
|
|
203
|
+
const req = this.#findRequest(state, requestId);
|
|
204
|
+
this.#owns(verifiedCaller, req.caller.consumer);
|
|
205
|
+
this.#pending(req, expectedVersion);
|
|
206
|
+
const next = { ...req, status: 'cancelled', version: req.version + 1, decidedAt: this.#now() };
|
|
207
|
+
this.#replaceRequest(state, next);
|
|
208
|
+
return next;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async decide(input) {
|
|
212
|
+
const decided = await this.#snapshotTransaction(async (state) => {
|
|
213
|
+
const req = this.#findRequest(state, input.requestId);
|
|
214
|
+
this.#pending(req, input.expectedVersion);
|
|
215
|
+
if (input.decision === 'deny') {
|
|
216
|
+
const next = { ...req, version: req.version + 1, status: 'denied', decidedAt: this.#now() };
|
|
217
|
+
this.#replaceRequest(state, next);
|
|
218
|
+
state.suppressions.push({ consumer: req.caller.consumer, kind: req.kind, requestId: req.id,
|
|
219
|
+
...(req.kind === 'access' && req.identity !== undefined ? { identity: req.identity } : {}) });
|
|
220
|
+
return next;
|
|
221
|
+
}
|
|
222
|
+
if (input.decision !== 'approve')
|
|
223
|
+
fail('invalid_decision');
|
|
224
|
+
await this.options.validateCaller(req.caller);
|
|
225
|
+
if (req.kind === 'create') {
|
|
226
|
+
const next = { ...req, version: req.version + 1, status: 'approved', decidedAt: this.#now(), operationId: randomUUID(), executionStatus: 'running' };
|
|
227
|
+
this.#replaceRequest(state, next);
|
|
228
|
+
return next;
|
|
229
|
+
}
|
|
230
|
+
const selected = input.identity ?? req.identity;
|
|
231
|
+
if (selected === undefined)
|
|
232
|
+
fail('identity_required');
|
|
233
|
+
const identity = reference(selected);
|
|
234
|
+
if (req.identity !== undefined && !equalIdentity(req.identity, identity))
|
|
235
|
+
fail('identity_mismatch');
|
|
236
|
+
this.#identityAvailable(state, identity);
|
|
237
|
+
const approvedSnapshot = snapshot(await this.options.resolveSnapshot(req.caller, identity));
|
|
238
|
+
const mode = input.mode ?? 'once';
|
|
239
|
+
if (mode !== 'once' && mode !== 'permanent')
|
|
240
|
+
fail('invalid_authorization_mode');
|
|
241
|
+
if (mode === 'once' && req.operation === undefined)
|
|
242
|
+
fail('once_operation_required');
|
|
243
|
+
if (req.operation !== undefined && !permits(approvedSnapshot, req.operation))
|
|
244
|
+
fail('operation_not_permitted');
|
|
245
|
+
if (input.reviewedSnapshot === undefined)
|
|
246
|
+
fail('review_snapshot_required');
|
|
247
|
+
if (canonical(snapshot(input.reviewedSnapshot)) !== canonical(approvedSnapshot))
|
|
248
|
+
fail('review_snapshot_changed');
|
|
249
|
+
const grant = {
|
|
250
|
+
id: randomUUID(), requestId: req.id, caller: req.caller, identity, snapshot: approvedSnapshot,
|
|
251
|
+
mode, version: 1, approvedAt: this.#now(), status: 'active',
|
|
252
|
+
...(mode === 'once' ? { expiresAt: this.#now() + (this.options.onceTtlMs ?? FIVE_MINUTES), operation: req.operation } : {}),
|
|
253
|
+
};
|
|
254
|
+
const next = { ...req, status: 'approved', version: req.version + 1, decidedAt: this.#now(), identity, mode, grantId: grant.id };
|
|
255
|
+
state.grants.push(grant);
|
|
256
|
+
this.#replaceRequest(state, next);
|
|
257
|
+
return next;
|
|
258
|
+
});
|
|
259
|
+
if (decided.kind !== 'create' || decided.status !== 'approved' || decided.operationId === undefined)
|
|
260
|
+
return decided;
|
|
261
|
+
try {
|
|
262
|
+
const result = await this.options.createIdentity(decided.operationId, decided.caller, copy(decided.parameters));
|
|
263
|
+
return await this.#finishCreation(decided.id, result);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// A native side effect may already exist. Never retry create or expose native error data.
|
|
267
|
+
return this.#transaction(async (state) => {
|
|
268
|
+
const req = this.#findRequest(state, decided.id);
|
|
269
|
+
if (req.executionStatus === 'succeeded' || req.executionStatus === 'deleted')
|
|
270
|
+
return req;
|
|
271
|
+
const next = { ...req, version: req.version + 1, executionStatus: 'unknown' };
|
|
272
|
+
this.#replaceRequest(state, next);
|
|
273
|
+
return next;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async reconcileCreation(requestId) {
|
|
278
|
+
const req = await this.#transaction(async (state) => this.#findRequest(state, requestId));
|
|
279
|
+
if (req.kind !== 'create' || req.status !== 'approved' || req.operationId === undefined)
|
|
280
|
+
fail('creation_not_approved');
|
|
281
|
+
if (req.executionStatus === 'succeeded' || req.executionStatus === 'deleted')
|
|
282
|
+
return req;
|
|
283
|
+
const result = await this.options.reconcileCreation?.(req.operationId, req.caller, copy(req.parameters));
|
|
284
|
+
return result === undefined ? req : this.#finishCreation(req.id, result);
|
|
285
|
+
}
|
|
286
|
+
async #finishCreation(requestId, result) {
|
|
287
|
+
const publicResult = { reference: reference(result.reference), label: text(result.label, 256) };
|
|
288
|
+
return this.#transaction(async (state) => {
|
|
289
|
+
const req = this.#findRequest(state, requestId);
|
|
290
|
+
if (req.kind !== 'create' || req.status !== 'approved')
|
|
291
|
+
fail('creation_not_approved');
|
|
292
|
+
if (req.result !== undefined && !equalIdentity(req.result.reference, publicResult.reference))
|
|
293
|
+
fail('creation_result_conflict');
|
|
294
|
+
if (req.executionStatus === 'deleted' || state.deletedIdentities.some(ref => equalIdentity(ref, publicResult.reference))) {
|
|
295
|
+
const deleted = { ...req, version: req.version + 1, executionStatus: 'deleted', result: publicResult };
|
|
296
|
+
this.#replaceRequest(state, deleted);
|
|
297
|
+
return deleted;
|
|
298
|
+
}
|
|
299
|
+
const next = { ...req, version: req.version + 1, executionStatus: 'succeeded', result: publicResult };
|
|
300
|
+
this.#replaceRequest(state, next);
|
|
301
|
+
return next;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async listGrants(identity) {
|
|
305
|
+
return this.#transaction(async (state) => state.grants.filter(grant => identity === undefined || equalIdentity(identity, grant.identity)));
|
|
306
|
+
}
|
|
307
|
+
async revoke(grantId, expectedVersion) {
|
|
308
|
+
return this.#transaction(async (state) => {
|
|
309
|
+
const grant = this.#findGrant(state, grantId);
|
|
310
|
+
if (grant.version !== expectedVersion)
|
|
311
|
+
fail('grant_conflict');
|
|
312
|
+
if (grant.status === 'revoked')
|
|
313
|
+
return grant;
|
|
314
|
+
const next = { ...grant, status: 'revoked', version: grant.version + 1, revokedAt: this.#now() };
|
|
315
|
+
this.#replaceGrant(state, next);
|
|
316
|
+
state.suppressions.push({ consumer: grant.caller.consumer, kind: 'access', identity: grant.identity, requestId: grant.requestId });
|
|
317
|
+
return next;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/** Manager-only explicit user retry. The old request and decision remain immutable history. */
|
|
321
|
+
async reauthorize(requestId, expectedVersion) {
|
|
322
|
+
return this.#transaction(async (state) => {
|
|
323
|
+
const req = this.#findRequest(state, requestId);
|
|
324
|
+
if (req.version !== expectedVersion)
|
|
325
|
+
fail('request_conflict');
|
|
326
|
+
if (req.status === 'pending' || (req.kind === 'create' && req.status === 'approved'))
|
|
327
|
+
fail('request_not_retryable');
|
|
328
|
+
await this.options.validateCaller(req.caller);
|
|
329
|
+
const details = req.kind === 'create' ? { kind: 'create', parameters: req.parameters }
|
|
330
|
+
: { kind: 'access', ...(req.identity === undefined ? {} : { identity: req.identity }), ...(req.operation === undefined ? {} : { operation: req.operation }) };
|
|
331
|
+
state.suppressions = state.suppressions.filter(block => !(block.consumer === req.caller.consumer && block.kind === req.kind
|
|
332
|
+
&& (block.identity === undefined || req.kind === 'create' || req.identity === undefined || equalIdentity(block.identity, req.identity))));
|
|
333
|
+
const fresh = this.#request(state, req.caller, randomUUID(), req.purpose, details);
|
|
334
|
+
const next = { ...fresh, retryOf: req.id };
|
|
335
|
+
this.#replaceRequest(state, next);
|
|
336
|
+
return next;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/** Call only for an authoritative disable/uninstall, never for a transient connection loss. */
|
|
340
|
+
async cancelPendingForConsumer(consumer) {
|
|
341
|
+
text(consumer, 256);
|
|
342
|
+
await this.#transaction(async (state) => {
|
|
343
|
+
state.requests = state.requests.map(req => req.caller.consumer === consumer && req.status === 'pending'
|
|
344
|
+
? { ...req, status: 'cancelled', version: req.version + 1, decidedAt: this.#now() } : req);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/** Host-only bridge for legacy catalog grants, serialized against deletion and ordinary approvals. */
|
|
348
|
+
async withIdentityAccess(identity, callback) {
|
|
349
|
+
return this.#transaction(async (state) => {
|
|
350
|
+
this.#identityAvailable(state, reference(identity));
|
|
351
|
+
// Publish an epoch before catalog changes, including an uncertain callback failure.
|
|
352
|
+
// Snapshot transactions must not commit ordinary use across a Host association change.
|
|
353
|
+
state.generation += 1;
|
|
354
|
+
await this.#write(state);
|
|
355
|
+
return callback();
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async open(verifiedCaller, grantId) {
|
|
359
|
+
return this.#snapshotTransaction(async (state) => {
|
|
360
|
+
const grant = this.#findGrant(state, grantId);
|
|
361
|
+
this.#owns(verifiedCaller, grant.caller.consumer);
|
|
362
|
+
if (grant.status !== 'active')
|
|
363
|
+
fail('grant_inactive');
|
|
364
|
+
this.#identityAvailable(state, grant.identity);
|
|
365
|
+
await this.options.validateCaller(grant.caller);
|
|
366
|
+
snapshot(await this.options.resolveSnapshot(grant.caller, grant.identity));
|
|
367
|
+
return { grantId, consumer: grant.caller.consumer, identity: grant.identity, grantVersion: grant.version,
|
|
368
|
+
expiresAt: Math.min(this.#now() + (this.options.leaseTtlMs ?? FIVE_MINUTES), grant.expiresAt ?? Infinity) };
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async admit(verifiedCaller, lease, requestedOperation, executionId) {
|
|
372
|
+
const actual = operation(requestedOperation);
|
|
373
|
+
text(executionId, 256);
|
|
374
|
+
return this.#snapshotTransaction(async (state) => {
|
|
375
|
+
// An execution ID is a query key, never permission to replay its side effects.
|
|
376
|
+
if (state.executions.some(value => value.id === executionId))
|
|
377
|
+
fail('execution_already_admitted');
|
|
378
|
+
const grant = await this.#checkLease(state, verifiedCaller, lease, actual);
|
|
379
|
+
if (grant.status !== 'active')
|
|
380
|
+
fail('grant_inactive');
|
|
381
|
+
if (grant.mode === 'once' && grant.operation?.fingerprint !== actual.fingerprint)
|
|
382
|
+
fail('operation_mismatch');
|
|
383
|
+
const execution = {
|
|
384
|
+
id: executionId, grantId: grant.id, consumer: grant.caller.consumer, fingerprint: actual.fingerprint,
|
|
385
|
+
grantVersion: grant.version, operation: actual, admittedAt: this.#now(), status: 'admitted',
|
|
386
|
+
};
|
|
387
|
+
state.executions.push(execution);
|
|
388
|
+
if (grant.mode === 'once')
|
|
389
|
+
this.#replaceGrant(state, { ...grant, status: 'consumed', version: grant.version + 1, executionId });
|
|
390
|
+
return execution;
|
|
391
|
+
}, lease.expiresAt);
|
|
392
|
+
}
|
|
393
|
+
async assertExecutionActive(verifiedCaller, lease, executionId) {
|
|
394
|
+
await this.#snapshotTransaction(async (state) => {
|
|
395
|
+
const execution = state.executions.find(value => value.id === executionId);
|
|
396
|
+
if (execution === undefined || execution.grantId !== lease.grantId || execution.status !== 'admitted')
|
|
397
|
+
fail('execution_inactive');
|
|
398
|
+
this.#owns(verifiedCaller, execution.consumer);
|
|
399
|
+
const grant = this.#findGrant(state, lease.grantId);
|
|
400
|
+
if (execution.grantVersion !== lease.grantVersion)
|
|
401
|
+
fail('grant_conflict');
|
|
402
|
+
const expectedVersion = grant.mode === 'once' ? execution.grantVersion + 1 : execution.grantVersion;
|
|
403
|
+
if (grant.version !== expectedVersion || (grant.status !== 'active' && !(grant.status === 'consumed' && grant.executionId === executionId)))
|
|
404
|
+
fail('grant_inactive');
|
|
405
|
+
await this.#checkLease(state, verifiedCaller, { ...lease, grantVersion: expectedVersion }, execution.operation);
|
|
406
|
+
}, lease.expiresAt);
|
|
407
|
+
}
|
|
408
|
+
async recordExecution(verifiedCaller, executionId, status) {
|
|
409
|
+
if (!['succeeded', 'failed', 'unknown'].includes(status))
|
|
410
|
+
fail('invalid_execution_status');
|
|
411
|
+
return this.#transaction(async (state) => {
|
|
412
|
+
const index = state.executions.findIndex(value => value.id === executionId);
|
|
413
|
+
const execution = state.executions[index];
|
|
414
|
+
if (execution === undefined)
|
|
415
|
+
fail('execution_not_found');
|
|
416
|
+
this.#owns(verifiedCaller, execution.consumer);
|
|
417
|
+
if (execution.status !== 'admitted') {
|
|
418
|
+
if (execution.status !== status)
|
|
419
|
+
fail('execution_result_conflict');
|
|
420
|
+
return execution;
|
|
421
|
+
}
|
|
422
|
+
const next = { ...execution, status, finishedAt: this.#now() };
|
|
423
|
+
state.executions[index] = next;
|
|
424
|
+
return next;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async getExecution(verifiedCaller, executionId) {
|
|
428
|
+
return this.#transaction(async (state) => {
|
|
429
|
+
const result = state.executions.find(value => value.id === executionId);
|
|
430
|
+
if (result === undefined)
|
|
431
|
+
fail('execution_not_found');
|
|
432
|
+
this.#owns(verifiedCaller, result.consumer);
|
|
433
|
+
return result;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/** Prepare checks associations and may set the catalog deletion fence before native execution. */
|
|
437
|
+
async withIdentityDeletion(identity, prepare, execute) {
|
|
438
|
+
const target = reference(identity);
|
|
439
|
+
// Serialize duplicate deletes of this identity without blocking the shared ledger
|
|
440
|
+
// during native I/O. A crash releases this lock but leaves the durable fence.
|
|
441
|
+
await this.#locked(async () => {
|
|
442
|
+
const shouldExecute = await this.#locked(async () => {
|
|
443
|
+
const state = await this.#read();
|
|
444
|
+
this.#expire(state);
|
|
445
|
+
if (state.grants.some(grant => equalIdentity(grant.identity, target) && grant.status === 'active'))
|
|
446
|
+
fail('identity_has_grants');
|
|
447
|
+
if (state.deletedIdentities.some(ref => equalIdentity(ref, target)))
|
|
448
|
+
return false;
|
|
449
|
+
await prepare();
|
|
450
|
+
if (!state.deletingIdentities.some(ref => equalIdentity(ref, target)))
|
|
451
|
+
state.deletingIdentities.push(target);
|
|
452
|
+
state.generation += 1;
|
|
453
|
+
await this.#write(state);
|
|
454
|
+
return true;
|
|
455
|
+
});
|
|
456
|
+
if (!shouldExecute)
|
|
457
|
+
return;
|
|
458
|
+
// A failed/uncertain execute leaves the fence for explicit reconciliation; no grant can race in.
|
|
459
|
+
await execute();
|
|
460
|
+
await this.#transaction(async (state) => {
|
|
461
|
+
this.#completeDeletion(state, target);
|
|
462
|
+
});
|
|
463
|
+
}, join(this.stateRoot, `authorization-delete-${digest(target)}.lock`));
|
|
464
|
+
this.#notify();
|
|
465
|
+
}
|
|
466
|
+
/** Host-only recovery of previously fenced deletions; absence alone never authorizes deletion. */
|
|
467
|
+
async reconcileDeletions(input) {
|
|
468
|
+
const catalogFences = input.catalogFences.map(reference);
|
|
469
|
+
await this.#snapshotTransaction(async (state) => {
|
|
470
|
+
for (const target of catalogFences) {
|
|
471
|
+
if (!state.deletedIdentities.some(ref => equalIdentity(ref, target))
|
|
472
|
+
&& !state.deletingIdentities.some(ref => equalIdentity(ref, target)))
|
|
473
|
+
state.deletingIdentities.push(target);
|
|
474
|
+
}
|
|
475
|
+
for (const target of [...state.deletingIdentities]) {
|
|
476
|
+
if (state.grants.some(grant => equalIdentity(grant.identity, target) && grant.status === 'active'))
|
|
477
|
+
continue;
|
|
478
|
+
if (await input.isAbsent(copy(target)))
|
|
479
|
+
this.#completeDeletion(state, target);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
#completeDeletion(state, target) {
|
|
484
|
+
state.deletingIdentities = state.deletingIdentities.filter(ref => !equalIdentity(ref, target));
|
|
485
|
+
if (!state.deletedIdentities.some(ref => equalIdentity(ref, target)))
|
|
486
|
+
state.deletedIdentities.push(target);
|
|
487
|
+
state.requests = state.requests.map(req => req.kind === 'create' && req.result !== undefined
|
|
488
|
+
&& equalIdentity(req.result.reference, target) && req.executionStatus !== 'deleted'
|
|
489
|
+
? { ...req, executionStatus: 'deleted', version: req.version + 1 } : req);
|
|
490
|
+
}
|
|
491
|
+
async #checkLease(state, verifiedCaller, lease, actual) {
|
|
492
|
+
const grant = this.#findGrant(state, lease.grantId);
|
|
493
|
+
this.#owns(verifiedCaller, grant.caller.consumer);
|
|
494
|
+
if (lease.consumer !== grant.caller.consumer || !equalIdentity(lease.identity, grant.identity))
|
|
495
|
+
fail('identity_mismatch');
|
|
496
|
+
if (lease.expiresAt <= this.#now())
|
|
497
|
+
fail('lease_expired');
|
|
498
|
+
if (grant.version !== lease.grantVersion)
|
|
499
|
+
fail('grant_conflict');
|
|
500
|
+
this.#identityAvailable(state, grant.identity);
|
|
501
|
+
await this.options.validateCaller(grant.caller);
|
|
502
|
+
const current = snapshot(await this.options.resolveSnapshot(grant.caller, grant.identity));
|
|
503
|
+
if (lease.expiresAt <= this.#now())
|
|
504
|
+
fail('lease_expired');
|
|
505
|
+
if (current.version !== grant.snapshot.version || !permits(grant.snapshot, actual) || !permits(current, actual))
|
|
506
|
+
fail('operation_not_permitted');
|
|
507
|
+
return grant;
|
|
508
|
+
}
|
|
509
|
+
#identityAvailable(state, identity) {
|
|
510
|
+
if ([...state.deletingIdentities, ...state.deletedIdentities].some(ref => equalIdentity(ref, identity)))
|
|
511
|
+
fail('identity_unavailable');
|
|
512
|
+
}
|
|
513
|
+
#owns(source, consumer) { if (source.consumer !== consumer)
|
|
514
|
+
fail('caller_mismatch'); }
|
|
515
|
+
#findRequest(state, id) {
|
|
516
|
+
return state.requests.find(req => req.id === id) ?? fail('request_not_found');
|
|
517
|
+
}
|
|
518
|
+
#findGrant(state, id) {
|
|
519
|
+
return state.grants.find(grant => grant.id === id) ?? fail('grant_not_found');
|
|
520
|
+
}
|
|
521
|
+
#pending(req, version) {
|
|
522
|
+
if (req.version !== version)
|
|
523
|
+
fail('request_conflict');
|
|
524
|
+
if (req.status !== 'pending')
|
|
525
|
+
fail('request_not_pending');
|
|
526
|
+
}
|
|
527
|
+
#replaceRequest(state, req) {
|
|
528
|
+
state.requests[state.requests.findIndex(value => value.id === req.id)] = req;
|
|
529
|
+
}
|
|
530
|
+
#replaceGrant(state, grant) {
|
|
531
|
+
state.grants[state.grants.findIndex(value => value.id === grant.id)] = grant;
|
|
532
|
+
}
|
|
533
|
+
#expire(state) {
|
|
534
|
+
const now = this.#now();
|
|
535
|
+
state.requests = state.requests.map(req => req.status === 'pending' && req.expiresAt <= now
|
|
536
|
+
? { ...req, status: 'expired', version: req.version + 1, decidedAt: now } : req);
|
|
537
|
+
state.grants = state.grants.map(grant => grant.status === 'active' && grant.mode === 'once' && grant.expiresAt <= now
|
|
538
|
+
? { ...grant, status: 'expired', version: grant.version + 1 } : grant);
|
|
539
|
+
}
|
|
540
|
+
async #transaction(action) {
|
|
541
|
+
let changed = false;
|
|
542
|
+
const result = await this.#locked(async () => {
|
|
543
|
+
const state = await this.#read();
|
|
544
|
+
const before = JSON.stringify(state);
|
|
545
|
+
this.#expire(state);
|
|
546
|
+
// Persist expiration even if the requested action subsequently fails.
|
|
547
|
+
if (JSON.stringify(state) !== before) {
|
|
548
|
+
state.generation += 1;
|
|
549
|
+
await this.#write(state);
|
|
550
|
+
changed = true;
|
|
551
|
+
}
|
|
552
|
+
const current = JSON.stringify(state);
|
|
553
|
+
const result = await action(state);
|
|
554
|
+
if (JSON.stringify(state) !== current) {
|
|
555
|
+
state.generation += 1;
|
|
556
|
+
await this.#write(state);
|
|
557
|
+
changed = true;
|
|
558
|
+
}
|
|
559
|
+
return copy(result);
|
|
560
|
+
});
|
|
561
|
+
if (changed)
|
|
562
|
+
this.#notify();
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
#notify() { for (const listener of this.#listeners) {
|
|
566
|
+
try {
|
|
567
|
+
listener();
|
|
568
|
+
}
|
|
569
|
+
catch { /* Notifications are advisory. */ }
|
|
570
|
+
} }
|
|
571
|
+
/** The callback may read native/catalog state but must not perform external mutations.
|
|
572
|
+
* Re-evaluate on a ledger CAS conflict, including revocation, deletion and Host association
|
|
573
|
+
* changes. Native waits never hold the shared authorization lock.
|
|
574
|
+
*/
|
|
575
|
+
async #snapshotTransaction(action, leaseDeadline = Infinity) {
|
|
576
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
577
|
+
const state = await this.#transaction(async (current) => current);
|
|
578
|
+
const generation = state.generation;
|
|
579
|
+
const before = JSON.stringify(state);
|
|
580
|
+
const result = await action(state);
|
|
581
|
+
const committed = await this.#locked(async () => {
|
|
582
|
+
const current = await this.#read();
|
|
583
|
+
if (current.generation !== generation)
|
|
584
|
+
return false;
|
|
585
|
+
if (leaseDeadline <= this.#now())
|
|
586
|
+
fail('lease_expired');
|
|
587
|
+
const unexpired = JSON.stringify(current);
|
|
588
|
+
this.#expire(current);
|
|
589
|
+
if (JSON.stringify(current) !== unexpired) {
|
|
590
|
+
current.generation += 1;
|
|
591
|
+
await this.#write(current);
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
if (JSON.stringify(state) !== before) {
|
|
595
|
+
state.generation = generation + 1;
|
|
596
|
+
await this.#write(state);
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
});
|
|
600
|
+
if (committed) {
|
|
601
|
+
if (JSON.stringify(state) !== before)
|
|
602
|
+
this.#notify();
|
|
603
|
+
return copy(result);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return fail('authorization_conflict');
|
|
607
|
+
}
|
|
608
|
+
async #locked(action, path = this.#lockPath) {
|
|
609
|
+
const release = await lockfile.lock(path, { realpath: false, stale: 30_000, update: 10_000, retries: { retries: 500, minTimeout: 20, maxTimeout: 20 } });
|
|
610
|
+
try {
|
|
611
|
+
return await action();
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
await release();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async #read() {
|
|
618
|
+
const raw = await readFile(this.statePath);
|
|
619
|
+
if (raw.length > 16 * 1024 * 1024)
|
|
620
|
+
fail('authorization_store_corrupt');
|
|
621
|
+
try {
|
|
622
|
+
const value = JSON.parse(raw.toString('utf8'));
|
|
623
|
+
if (value.schema !== SCHEMA || !Number.isSafeInteger(value.generation) || value.generation < 0
|
|
624
|
+
|| !Array.isArray(value.requests) || !Array.isArray(value.grants) || !Array.isArray(value.executions)
|
|
625
|
+
|| !Array.isArray(value.deletedIdentities) || !Array.isArray(value.deletingIdentities) || !Array.isArray(value.suppressions))
|
|
626
|
+
fail('authorization_store_corrupt');
|
|
627
|
+
if (new Set(value.requests.map(req => req.id)).size !== value.requests.length
|
|
628
|
+
|| new Set(value.grants.map(grant => grant.id)).size !== value.grants.length
|
|
629
|
+
|| new Set(value.executions.map(execution => execution.id)).size !== value.executions.length)
|
|
630
|
+
fail('authorization_store_corrupt');
|
|
631
|
+
for (const req of value.requests) {
|
|
632
|
+
text(req.id, 256);
|
|
633
|
+
caller(req.caller);
|
|
634
|
+
text(req.purpose);
|
|
635
|
+
text(req.fingerprint);
|
|
636
|
+
if (!['create', 'access'].includes(req.kind) || !['pending', 'approved', 'denied', 'cancelled', 'expired'].includes(req.status)
|
|
637
|
+
|| !Number.isSafeInteger(req.version) || req.version < 1 || !Number.isFinite(req.expiresAt))
|
|
638
|
+
fail('authorization_store_corrupt');
|
|
639
|
+
if (req.kind === 'access') {
|
|
640
|
+
if (req.identity)
|
|
641
|
+
reference(req.identity);
|
|
642
|
+
if (req.operation)
|
|
643
|
+
operation(req.operation);
|
|
644
|
+
}
|
|
645
|
+
if (req.kind === 'create') {
|
|
646
|
+
text(req.parameters.label);
|
|
647
|
+
text(req.parameters.domain);
|
|
648
|
+
text(req.parameters.path, 2048);
|
|
649
|
+
if (req.result)
|
|
650
|
+
reference(req.result.reference);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
for (const grant of value.grants) {
|
|
654
|
+
text(grant.id);
|
|
655
|
+
caller(grant.caller);
|
|
656
|
+
reference(grant.identity);
|
|
657
|
+
snapshot(grant.snapshot);
|
|
658
|
+
if (!['active', 'consumed', 'revoked', 'expired'].includes(grant.status) || !['once', 'permanent'].includes(grant.mode)
|
|
659
|
+
|| !Number.isSafeInteger(grant.version) || grant.version < 1)
|
|
660
|
+
fail('authorization_store_corrupt');
|
|
661
|
+
if (grant.mode === 'once') {
|
|
662
|
+
if (!Number.isFinite(grant.expiresAt) || grant.operation === undefined)
|
|
663
|
+
fail('authorization_store_corrupt');
|
|
664
|
+
operation(grant.operation);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const execution of value.executions) {
|
|
668
|
+
text(execution.id);
|
|
669
|
+
text(execution.consumer);
|
|
670
|
+
operation(execution.operation);
|
|
671
|
+
if (!['admitted', 'succeeded', 'failed', 'unknown'].includes(execution.status))
|
|
672
|
+
fail('authorization_store_corrupt');
|
|
673
|
+
}
|
|
674
|
+
for (const identity of [...value.deletedIdentities, ...value.deletingIdentities])
|
|
675
|
+
reference(identity);
|
|
676
|
+
for (const block of value.suppressions) {
|
|
677
|
+
text(block.consumer, 256);
|
|
678
|
+
text(block.requestId, 256);
|
|
679
|
+
if (block.kind !== 'create' && block.kind !== 'access')
|
|
680
|
+
fail('authorization_store_corrupt');
|
|
681
|
+
if (block.identity !== undefined)
|
|
682
|
+
reference(block.identity);
|
|
683
|
+
}
|
|
684
|
+
return value;
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return fail('authorization_store_corrupt');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async #write(state) {
|
|
691
|
+
const serialized = JSON.stringify(state);
|
|
692
|
+
if (Buffer.byteLength(serialized) > 16 * 1024 * 1024)
|
|
693
|
+
fail('authorization_store_full');
|
|
694
|
+
const temporary = `${this.statePath}.${randomUUID()}.tmp`;
|
|
695
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
696
|
+
try {
|
|
697
|
+
await file.writeFile(serialized);
|
|
698
|
+
await file.sync();
|
|
699
|
+
await file.close();
|
|
700
|
+
this.options.fault?.('before_rename');
|
|
701
|
+
await rename(temporary, this.statePath);
|
|
702
|
+
const directory = await open(this.stateRoot, 'r');
|
|
703
|
+
try {
|
|
704
|
+
await directory.sync();
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
await directory.close();
|
|
708
|
+
}
|
|
709
|
+
this.options.fault?.('after_rename');
|
|
710
|
+
}
|
|
711
|
+
finally {
|
|
712
|
+
await file.close();
|
|
713
|
+
await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
|
|
714
|
+
throw error; });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
//# sourceMappingURL=authorization.js.map
|