@datafog/core-fogvault-legacy-browser 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +66 -2
- package/THIRD_PARTY_NOTICES.md +21 -0
- package/backup-read.mjs +63 -0
- package/error.mjs +1 -0
- package/index.d.ts +16 -0
- package/index.mjs +75 -0
- package/kdf-worker.mjs +3463 -0
- package/package.json +45 -4
- package/vault-read.mjs +119 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DataFog
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Import supported Core reference backups
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This optional `1.0.0` package reads only the non-shipping Core
|
|
4
|
+
workspace example's `datafog-workspace-backup` envelope versions 1 and 2. It
|
|
5
|
+
**does not establish compatibility with an unidentified shipping extension**.
|
|
6
|
+
Backup envelope versions are separate from the Core snapshot decoder versions.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --save-exact @datafog/core-fogvault-legacy-browser@1.0.0
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`LegacyBackupImport.open` authenticates the complete canonical archive, decrypts
|
|
13
|
+
it using the existing legacy algorithms, and validates its namespace/full head
|
|
14
|
+
through the initialized actual Core WASM decoder. The returned opaque session
|
|
15
|
+
exposes metadata/counts, not a plaintext bundle or legacy keys. `commit` requires
|
|
16
|
+
literal `approved: true`, fresh request/operation/record IDs and a fresh unlocked
|
|
17
|
+
FogVault destination. The existing browser adapter owns recovery, exact save,
|
|
18
|
+
activation, record/head CAS, occupied-destination rejection and reconciliation.
|
|
19
|
+
Source archive bytes are borrowed unchanged; only importer-owned copies are wiped.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const staged = await LegacyBackupImport.open({Workspace, archive, credential,
|
|
23
|
+
method: 'password', signal: controller.signal});
|
|
24
|
+
const metadata = staged.preview(); // Review locally before approval.
|
|
25
|
+
const owner = await staged.commit({vault: freshUnlockedVault, recordId,
|
|
26
|
+
operationId, requestId, approved: true});
|
|
27
|
+
// owner is the maintained FogVaultWorkspace facade.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Password strings preserve the reference format's exact UTF-8 bytes, without
|
|
31
|
+
normalization; recovery codes use its existing DFR1 syntax. The original fixed
|
|
32
|
+
Argon2id v1.3/t3/p1 profiles remain 64 MiB (v1) and 256 MiB (v2), with no cheaper
|
|
33
|
+
fallback. Recovery uses the original HKDF-SHA-256/AES-GCM path. The importer uses
|
|
34
|
+
an actual disposable bundled libsodium worker and WebCrypto, not another crypto
|
|
35
|
+
implementation. `vault-read.mjs`, `backup-read.mjs` and the worker derive directly
|
|
36
|
+
from `examples/workspace-extension/src/{vault.js,backup.js,kdf-worker.js}`; only
|
|
37
|
+
read/authenticate helpers are retained. The original example remains the fixture
|
|
38
|
+
producer, so tests do not validate an importer against its own serializer.
|
|
39
|
+
|
|
40
|
+
Call `close` or abort the supplied signal to discard a session. Cancellation
|
|
41
|
+
terminates a pending KDF; after publication starts it locks the exclusive fresh
|
|
42
|
+
destination. A late persisted save may require receipt reconciliation and fresh
|
|
43
|
+
reopen; cancellation never promises rollback. Uncertain publication returns
|
|
44
|
+
`import_requires_reopen` with the exact request ID and `requiresReopen: true`.
|
|
45
|
+
A cleanup failure cannot hide that outcome: the error also carries
|
|
46
|
+
`cleanupFailed: true`; explicit close reports content-free `cleanup_failed`. Successful commit consumes the
|
|
47
|
+
session and removes its abort listener, so later cancellation does not lock the
|
|
48
|
+
returned owner. Serialize imports and destination creation/KDF work in the host.
|
|
49
|
+
JavaScript strings, CryptoKey cleanup and browser memory erasure are best effort.
|
|
50
|
+
The 24 MiB archive / 16 MiB bundle caps do not establish peak-memory qualification.
|
|
51
|
+
|
|
52
|
+
Build with `npm ci --legacy-peer-deps --ignore-scripts` and `npm run build`.
|
|
53
|
+
The installed runner accepts explicit Core and FogVault tarballs:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
python scripts/verify-fogvault-legacy.py --prepared PREPARED.json --output REPORT.json --engine chromium
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It packs this package plus the actual Core browser adapter, installs all four
|
|
60
|
+
packages in an isolated consumer, verifies installed bytes, runs TypeScript and
|
|
61
|
+
real browser/KDF/IndexedDB cases, and retains named evidence. Browser workers need
|
|
62
|
+
same-origin script loading and a CSP permitting WebAssembly (`wasm-unsafe-eval`).
|
|
63
|
+
The preparation marker must bind both consumed tarballs and the exact current
|
|
64
|
+
Core build inputs; a stale marker fails before the browser starts.
|
|
65
|
+
No RPC broker, synthetic fault control, legacy export API or credential-creation
|
|
66
|
+
API is included in the maintained importer package. Native legacy import and a
|
|
67
|
+
final v1 schema upgrade are separate scope; this package cannot close those gates.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Bundled legacy KDF notices
|
|
2
|
+
|
|
3
|
+
The bundled worker includes libsodium-wrappers-sumo 0.8.4 and libsodium-sumo
|
|
4
|
+
0.8.4. Both installed distributions carry the following notice:
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2015-2026
|
|
7
|
+
Ahmad Ben Mrad <batikhsouri at gmail dot org>
|
|
8
|
+
Frank Denis <j at pureftpd dot org>
|
|
9
|
+
Ryan Lester <ryan at cyph dot com>
|
|
10
|
+
|
|
11
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
12
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
13
|
+
copyright notice and this permission notice appear in all copies.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
16
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
18
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
19
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
20
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
21
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/backup-read.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {LegacyImportError as HostError} from "./error.mjs";
|
|
2
|
+
import {validateVault, validateRecord, openVault, randomId, toBase64url, fromBase64url} from "./vault-read.mjs";
|
|
3
|
+
|
|
4
|
+
export const MAX_BACKUP_BYTES = 24 * 1024 * 1024;
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const vaultFields = ["vaultId", "vaultKeyId", "workspaceId", "salt", "rootNonce", "wrappedRoot"];
|
|
7
|
+
const recordFields = ["vaultId", "workspaceId", "keyId", "keyNonce", "wrappedKey", "nonce", "ciphertext"];
|
|
8
|
+
function fields(value, expected) {
|
|
9
|
+
if (!value || typeof value !== "object" || Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) throw new HostError("invalid_backup");
|
|
10
|
+
}
|
|
11
|
+
function payload(vault, record, exportedAt, version = 2) {
|
|
12
|
+
return {format: "datafog-workspace-backup", version, exportedAt,
|
|
13
|
+
vault: {version: vault.version, profile: vault.profile, ...Object.fromEntries(vaultFields.map(key => [key, toBase64url(vault[key])])),
|
|
14
|
+
recovery: {version: vault.recovery.version, salt: toBase64url(vault.recovery.salt), nonce: toBase64url(vault.recovery.nonce), wrappedRoot: toBase64url(vault.recovery.wrappedRoot)}},
|
|
15
|
+
workspace: {version: record.version, head: {revision: record.head.revision, commitId: toBase64url(new Uint8Array(record.head.commitId))},
|
|
16
|
+
...Object.fromEntries(recordFields.map(key => [key, toBase64url(record[key])]))}};
|
|
17
|
+
}
|
|
18
|
+
function authentication(body, nonce) {
|
|
19
|
+
return {name: "AES-GCM", iv: nonce, tagLength: 128,
|
|
20
|
+
additionalData: encoder.encode(`DFGB${body.version}:AUTH:${JSON.stringify(body)}`)};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function decodeBackup(bytes) {
|
|
24
|
+
if (!(bytes instanceof Uint8Array)) throw new HostError("invalid_backup");
|
|
25
|
+
if (bytes.length > MAX_BACKUP_BYTES) throw new HostError("resource_limit");
|
|
26
|
+
try {
|
|
27
|
+
const source = new TextDecoder("utf-8", {fatal: true, ignoreBOM: true}).decode(bytes);
|
|
28
|
+
const file = JSON.parse(source);
|
|
29
|
+
fields(file, ["payload", "nonce", "tag"]);
|
|
30
|
+
const body = file.payload;
|
|
31
|
+
fields(body, ["format", "version", "exportedAt", "vault", "workspace"]);
|
|
32
|
+
if (body.format !== "datafog-workspace-backup" || ![1, 2].includes(body.version) || typeof body.exportedAt !== "string" || body.exportedAt.length !== 24 || new Date(body.exportedAt).toISOString() !== body.exportedAt) throw new HostError("invalid_backup");
|
|
33
|
+
const v = body.vault, r = body.workspace;
|
|
34
|
+
fields(v, ["version", "profile", ...vaultFields, "recovery"]);
|
|
35
|
+
fields(v.recovery, ["version", "salt", "nonce", "wrappedRoot"]);
|
|
36
|
+
fields(r, ["version", "head", ...recordFields]); fields(r.head, ["revision", "commitId"]);
|
|
37
|
+
if (v.version !== 2 || v.profile !== body.version) throw new HostError("invalid_backup");
|
|
38
|
+
const vaultSizes = {vaultId: 16, vaultKeyId: 16, workspaceId: 16, salt: 16, rootNonce: 12, wrappedRoot: 48};
|
|
39
|
+
const recordSizes = {vaultId: 16, workspaceId: 16, keyId: 16, keyNonce: 12, wrappedKey: 48, nonce: 12, ciphertext: 16 * 1024 * 1024 + 16};
|
|
40
|
+
const vault = {version: v.version, profile: v.profile, accessGeneration: randomId(),
|
|
41
|
+
...Object.fromEntries(vaultFields.map(key => [key, fromBase64url(v[key], vaultSizes[key])])),
|
|
42
|
+
recovery: {version: v.recovery.version, salt: fromBase64url(v.recovery.salt, 16), nonce: fromBase64url(v.recovery.nonce, 12), wrappedRoot: fromBase64url(v.recovery.wrappedRoot, 48)}};
|
|
43
|
+
const record = {version: r.version, head: {revision: r.head.revision, commitId: Array.from(fromBase64url(r.head.commitId, 16))},
|
|
44
|
+
...Object.fromEntries(recordFields.map(key => [key, fromBase64url(r[key], recordSizes[key])]))};
|
|
45
|
+
validateVault(vault); validateRecord(record, vault);
|
|
46
|
+
const nonce = fromBase64url(file.nonce, 12), tag = fromBase64url(file.tag, 16);
|
|
47
|
+
if (nonce.length !== 12 || tag.length !== 16 || JSON.stringify({payload: payload(vault, record, body.exportedAt, body.version), nonce: toBase64url(nonce), tag: toBase64url(tag)}) !== source) throw new HostError("invalid_backup");
|
|
48
|
+
return {vault, record, exportedAt: body.exportedAt, body, nonce, tag};
|
|
49
|
+
} catch { throw new HostError("invalid_backup"); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Authenticate every exported field, even the unused credential slot, before staging. */
|
|
53
|
+
export async function openBackup(bytes, credential, method, signal) {
|
|
54
|
+
const decoded = decodeBackup(bytes);
|
|
55
|
+
const opened = await openVault(credential, decoded.vault, decoded.record, signal, method);
|
|
56
|
+
try {
|
|
57
|
+
await crypto.subtle.decrypt(authentication(decoded.body, decoded.nonce), opened.dataKey, decoded.tag);
|
|
58
|
+
return {vault: decoded.vault, record: decoded.record, exportedAt: decoded.exportedAt, ...opened};
|
|
59
|
+
} catch {
|
|
60
|
+
opened.bundle.fill(0);
|
|
61
|
+
throw new HostError("invalid_backup");
|
|
62
|
+
}
|
|
63
|
+
}
|
package/error.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class LegacyImportError extends Error { constructor(code){super(code);this.name='LegacyImportError';this.code=code;} }
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type {Workspace,WorkspaceHead} from '@datafog/wasm';
|
|
2
|
+
import type {FogVaultWorkspace,WorkspaceVaultOptions} from '@datafog/core-fogvault-browser';
|
|
3
|
+
export class LegacyImportError extends Error {readonly code:string;readonly requestId?:Uint8Array;readonly requiresReopen?:boolean;readonly cleanupFailed?:boolean;}
|
|
4
|
+
export interface LegacyOpenOptions {
|
|
5
|
+
Workspace:typeof Workspace; archive:Uint8Array; credential:string; method?:'password'|'recovery';
|
|
6
|
+
signal?:AbortSignal; expectedNamespace?:Uint8Array; expectedHead?:WorkspaceHead;
|
|
7
|
+
}
|
|
8
|
+
export class LegacyBackupImport {
|
|
9
|
+
private constructor();
|
|
10
|
+
static open(options:LegacyOpenOptions):Promise<LegacyBackupImport>;
|
|
11
|
+
preview():{exportedAt:string;namespace:Uint8Array;head:WorkspaceHead;counts:ReturnType<Workspace['counts']>};
|
|
12
|
+
/** Fresh unlocked destination exclusively owned by this operation. Exact literal approval required. */
|
|
13
|
+
commit(options:Omit<WorkspaceVaultOptions,'Workspace'> & {operationId:Uint8Array;requestId:Uint8Array;approved:true}):Promise<FogVaultWorkspace>;
|
|
14
|
+
/** Wipes the owned bundle; once publication starts also locks the destination. */
|
|
15
|
+
close():Promise<void>;
|
|
16
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {FogVaultWorkspace} from '@datafog/core-fogvault-browser';
|
|
2
|
+
import {openBackup,MAX_BACKUP_BYTES} from './backup-read.mjs';
|
|
3
|
+
import {LegacyImportError} from './error.mjs';
|
|
4
|
+
export {LegacyImportError};
|
|
5
|
+
const construct=Symbol('authenticated legacy session'),states=new WeakMap();
|
|
6
|
+
const fail=code=>{throw new LegacyImportError(code);};
|
|
7
|
+
const same=(a,b)=>a.length===b.length&&a.every((v,i)=>v===b[i]);
|
|
8
|
+
const typedPrototype=Object.getPrototypeOf(Uint8Array.prototype);
|
|
9
|
+
const bufferOf=Object.getOwnPropertyDescriptor(typedPrototype,'buffer').get;
|
|
10
|
+
const lengthOf=Object.getOwnPropertyDescriptor(typedPrototype,'length').get;
|
|
11
|
+
function bytes(value,length,maximum=MAX_BACKUP_BYTES){
|
|
12
|
+
if(!ArrayBuffer.isView(value)||Object.getPrototypeOf(value)!==Uint8Array.prototype)fail('invalid_transport');
|
|
13
|
+
for(const key of ['buffer','byteLength','byteOffset','length','constructor',Symbol.iterator])if(Object.hasOwn(value,key))fail('invalid_transport');
|
|
14
|
+
const buffer=bufferOf.call(value),size=lengthOf.call(value);
|
|
15
|
+
if(!(buffer instanceof ArrayBuffer)||buffer.detached)fail('invalid_transport');
|
|
16
|
+
if(size>maximum)fail('resource_limit');
|
|
17
|
+
if(length!==undefined&&size!==length)fail('invalid_identity');
|
|
18
|
+
const result=new Uint8Array(size);Uint8Array.prototype.set.call(result,value);return result;
|
|
19
|
+
}
|
|
20
|
+
function head(value){
|
|
21
|
+
if(!value||typeof value.revision!=='string'||!/^(0|[1-9][0-9]{0,19})$/.test(value.revision)||BigInt(value.revision)>18446744073709551615n)fail('invalid_head');
|
|
22
|
+
if(!(value.commitId instanceof Uint8Array)&&(!Array.isArray(value.commitId)||value.commitId.length!==16||value.commitId.some(v=>!Number.isInteger(v)||v<0||v>255)))fail('invalid_head');
|
|
23
|
+
const commitId=value.commitId instanceof Uint8Array?bytes(value.commitId,16):Uint8Array.from(value.commitId);
|
|
24
|
+
return {revision:value.revision,commitId:Array.from(commitId)};
|
|
25
|
+
}
|
|
26
|
+
function state(session){const s=states.get(session);if(!s||s.closed)fail('import_closed');return s;}
|
|
27
|
+
async function closeState(s){
|
|
28
|
+
s.closed=true;s.bundle?.fill(0);s.bundle=null;s.controller.abort();s.signal?.removeEventListener('abort',s.abort);
|
|
29
|
+
try{await s.vault?.lock();return true;}catch{s.cleanupFailed=true;return false;}
|
|
30
|
+
}
|
|
31
|
+
/** Import-only supported reference archives; never a generic archive decryptor. */
|
|
32
|
+
export class LegacyBackupImport {
|
|
33
|
+
constructor(token,s){if(token!==construct)fail('invalid_import');states.set(this,s);Object.freeze(this);}
|
|
34
|
+
static async open({Workspace,archive,credential,method='password',signal,expectedNamespace,expectedHead}){
|
|
35
|
+
if(!['password','recovery'].includes(method))fail('invalid_credentials');
|
|
36
|
+
if(typeof credential!=='string'||!credential.isWellFormed()||credential.length>1024)fail('invalid_credentials');
|
|
37
|
+
const wantedNamespace=expectedNamespace===undefined?undefined:bytes(expectedNamespace,16),wantedHead=expectedHead===undefined?undefined:head(expectedHead);
|
|
38
|
+
if(signal?.aborted)fail('cancelled');
|
|
39
|
+
const source=bytes(archive),controller=new AbortController();
|
|
40
|
+
const s={Workspace,controller,signal,closed:false,busy:false,bundle:null,vault:null};
|
|
41
|
+
s.abort=()=>{void closeState(s);};
|
|
42
|
+
signal?.addEventListener('abort',s.abort,{once:true});
|
|
43
|
+
let opened,workspace;
|
|
44
|
+
try{
|
|
45
|
+
if(signal?.aborted){s.abort();fail('cancelled');}
|
|
46
|
+
opened=await openBackup(source,credential,method,controller.signal);
|
|
47
|
+
if(s.closed||controller.signal.aborted)fail('cancelled');
|
|
48
|
+
workspace=Workspace.openBundle(opened.bundle);
|
|
49
|
+
const namespace=bytes(workspace.namespace(),16),saved=head(workspace.head());
|
|
50
|
+
if(!same(namespace,opened.vault.workspaceId)||saved.revision!==opened.record.head.revision||!same(saved.commitId,opened.record.head.commitId)
|
|
51
|
+
||wantedNamespace&&!same(namespace,wantedNamespace)||wantedHead&&(saved.revision!==wantedHead.revision||!same(saved.commitId,wantedHead.commitId)))fail('binding_mismatch');
|
|
52
|
+
s.namespace=namespace;s.head=saved;s.exportedAt=opened.exportedAt;s.counts=workspace.counts();s.bundle=opened.bundle;
|
|
53
|
+
return new LegacyBackupImport(construct,s);
|
|
54
|
+
}catch(error){opened?.bundle.fill(0);await closeState(s);throw error instanceof LegacyImportError?error:new LegacyImportError(error.code??'invalid_backup');}
|
|
55
|
+
finally{workspace?.close();source.fill(0);}
|
|
56
|
+
}
|
|
57
|
+
preview(){const s=state(this);return {exportedAt:s.exportedAt,namespace:Uint8Array.from(s.namespace),head:structuredClone(s.head),counts:structuredClone(s.counts)};}
|
|
58
|
+
async commit({vault,recordId,operationId,requestId,approved}){
|
|
59
|
+
const s=state(this);if(approved!==true)fail('approval_required');if(s.busy)fail('operation_in_progress');
|
|
60
|
+
const record=bytes(recordId,16),operation=bytes(operationId,16),request=bytes(requestId,16);
|
|
61
|
+
s.busy=true;s.vault=vault;let owner;
|
|
62
|
+
try{
|
|
63
|
+
owner=await FogVaultWorkspace.importAuthenticatedBundle({Workspace:s.Workspace,vault,recordId:record,bundle:s.bundle,
|
|
64
|
+
expectedNamespace:s.namespace,expectedHead:s.head,operationId:operation,requestId:request,approved:true});
|
|
65
|
+
if(s.closed||s.controller.signal.aborted){await owner.close();fail('cancelled');}
|
|
66
|
+
s.vault=null;await closeState(s);return owner;
|
|
67
|
+
}catch(error){
|
|
68
|
+
const uncertain=s.closed||s.controller.signal.aborted||['OutcomeUnknown','outcome_unknown','invalid_save_confirmation'].includes(error?.code);
|
|
69
|
+
const cleanupSucceeded=await closeState(s);
|
|
70
|
+
if(uncertain||!cleanupSucceeded||s.cleanupFailed){const failure=new LegacyImportError('import_requires_reopen');failure.requestId=request.slice();failure.requiresReopen=true;if(s.cleanupFailed)failure.cleanupFailed=true;throw failure;}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async close(){const s=states.get(this);if(s&&!s.closed)await closeState(s);if(s?.cleanupFailed)fail('cleanup_failed');}
|
|
75
|
+
}
|