opencode-session-port 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/package.json +56 -4
- package/src/core/contracts/crypto.ts +66 -0
- package/src/core/contracts/index.ts +13 -0
- package/src/core/contracts/json.ts +142 -0
- package/src/core/contracts/session.ts +12 -0
- package/src/core/crypto/container.ts +686 -0
- package/src/core/crypto/envelope.ts +232 -0
- package/src/core/crypto/errors.ts +52 -0
- package/src/core/crypto/gzip-stream.ts +388 -0
- package/src/core/crypto/index.ts +13 -0
- package/src/core/crypto/kdf.ts +101 -0
- package/src/core/crypto/policy.ts +202 -0
- package/src/core/crypto/zero.ts +33 -0
- package/src/core/restore/identity.ts +202 -0
- package/src/core/restore/index.ts +24 -0
- package/src/core/restore/planner.ts +1122 -0
- package/src/core/sessions/collector.ts +323 -0
- package/src/core/sessions/errors.ts +37 -0
- package/src/core/sessions/guards.ts +11 -0
- package/src/core/sessions/index.ts +29 -0
- package/src/core/sessions/policy.ts +80 -0
- package/src/core/sessions/port.ts +88 -0
- package/src/core/sessions/serialize.ts +15 -0
- package/src/core/sessions/unsettled.ts +35 -0
- package/src/integrations/omo/index.ts +28 -0
- package/src/tui/adapters/host-client.ts +254 -0
- package/src/tui/adapters/index.ts +72 -0
- package/src/tui/adapters/location-resolver.ts +281 -0
- package/src/tui/adapters/project-destinations.ts +271 -0
- package/src/tui/adapters/session-id-inventory.ts +378 -0
- package/src/tui/adapters/session-import.ts +189 -0
- package/src/tui/adapters/session-read.ts +115 -0
- package/src/tui/artifacts/files.ts +285 -0
- package/src/tui/orchestration/checkpoint.ts +160 -0
- package/src/tui/orchestration/index.ts +55 -0
- package/src/tui/orchestration/restore-auth.ts +94 -0
- package/src/tui/orchestration/restore-execution.ts +381 -0
- package/src/tui/orchestration/restore-preflight.ts +223 -0
- package/src/tui/orchestration/shared.ts +75 -0
- package/src/tui/ui/password-dialog.tsx +251 -0
- package/src/tui/ui/password-flow.ts +288 -0
- package/src/tui/ui/restore-project-selector.ts +62 -0
- package/src/tui/ui/restore-report-dialog.tsx +251 -0
- package/src/tui/ui/restore-report-flow.ts +94 -0
- package/src/tui/ui/sessionport-commands.tsx +65 -0
- package/src/tui/wiring/composition.ts +354 -0
- package/src/tui/wiring/experimental-profile.ts +86 -0
- package/src/tui/wiring/index.ts +51 -0
- package/src/tui/wiring/install.ts +108 -0
- package/src/tui/wiring/reports.ts +340 -0
- package/src/tui.ts +34 -0
- package/tsconfig.json +18 -0
- package/tui.ts +12 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injected checkpoint flow (Phase 4, bounded orchestration; `fix-57`).
|
|
3
|
+
*
|
|
4
|
+
* `runCheckpointFlow` wires the completed Phase 2/3 core and Phase 4 adapters
|
|
5
|
+
* into one testable sequence and returns only low-level structured facts:
|
|
6
|
+
*
|
|
7
|
+
* collectCheckpointPayload(port, { projectID, policy })
|
|
8
|
+
* → zero sessions ⇒ `no-sessions` outcome (P-04), before the password
|
|
9
|
+
* prompt, path resolution, crypto creation, and any filesystem write
|
|
10
|
+
* → requestPassword("checkpoint")
|
|
11
|
+
* → cancel ⇒ `canceled` outcome, before path resolution and write
|
|
12
|
+
* → resolveCheckpointOutputPath({ projectDirectory, explicitPath? }) (P-15)
|
|
13
|
+
* → cryptoFactory.create(cryptoPolicy) (explicit injected policy only)
|
|
14
|
+
* → crypto.seal(asOneShotByteSource(serializeCheckpointPayload(payload)), password)
|
|
15
|
+
* → writeEncryptedArtifact(path, sealedSource)
|
|
16
|
+
*
|
|
17
|
+
* Password handling: the resolved `Uint8Array` is treated as flow-owned and is
|
|
18
|
+
* held until `writeEncryptedArtifact` has consumed the sealed iterable (the
|
|
19
|
+
* real container's KDF/framing may pull the plaintext lazily), then
|
|
20
|
+
* best-effort zeroed in `finally` on success and every failure path. There is
|
|
21
|
+
* no plaintext fallback, no plaintext temp file (A-03), and no secure-deletion
|
|
22
|
+
* promise (A-07).
|
|
23
|
+
*
|
|
24
|
+
* Truthful outcomes: only `no-sessions`, `canceled`, and `written` are
|
|
25
|
+
* returned. Collection, path, crypto, and write failures propagate unchanged
|
|
26
|
+
* (for example `SessionCollectionError`, `CryptoContainerError`, `EEXIST`,
|
|
27
|
+
* `ArtifactFileError`) so an error, cancel, or empty collection can never be
|
|
28
|
+
* reported as a created artifact. No O-15 report/UX format is produced here —
|
|
29
|
+
* the caller owns the final report.
|
|
30
|
+
*
|
|
31
|
+
* Policy boundaries (all still OPEN; nothing inferred):
|
|
32
|
+
* - O-01/O-02/O-03: no production numeric policy, format, or cap exists in
|
|
33
|
+
* this module. `CryptoPolicy` and the `CheckpointCryptoFactory` are injected
|
|
34
|
+
* by the caller; tests use clearly test-only fakes.
|
|
35
|
+
* - O-10: this flow uses the existing collector's project-scoped enumeration
|
|
36
|
+
* and adds no child/subagent enumeration of its own; child-association
|
|
37
|
+
* semantics remain OPEN and the collector includes exactly what the
|
|
38
|
+
* explicit project scope returns.
|
|
39
|
+
* - O-15: report formats remain OPEN; only path/session-count/encrypted-byte
|
|
40
|
+
* facts cross this boundary.
|
|
41
|
+
* - P-15/O-08: the path itself comes from the injected
|
|
42
|
+
* `resolveCheckpointOutputPath`; this module never invents a path and never
|
|
43
|
+
* overwrites an existing artifact.
|
|
44
|
+
*
|
|
45
|
+
* Evidence boundary: exercised only by fake ports, fake password providers,
|
|
46
|
+
* fake crypto factories, and fake store functions. No real host call,
|
|
47
|
+
* filesystem write, KDF, or import is performed by this lane, and no runtime
|
|
48
|
+
* or compatibility claim follows.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
collectCheckpointPayload,
|
|
53
|
+
serializeCheckpointPayload,
|
|
54
|
+
type SessionCollectionPolicy,
|
|
55
|
+
type SessionReadPort,
|
|
56
|
+
} from "../../core/sessions/index.ts";
|
|
57
|
+
import type { ByteSource, CheckpointCryptoFactory, CryptoPolicy } from "../../core/contracts/index.ts";
|
|
58
|
+
import type { ResolveCheckpointOutputPathOptions } from "../artifacts/files.ts";
|
|
59
|
+
import {
|
|
60
|
+
asOneShotByteSource,
|
|
61
|
+
isRecordValue,
|
|
62
|
+
requireCallable,
|
|
63
|
+
requireNonEmptyString,
|
|
64
|
+
zeroPasswordBestEffort,
|
|
65
|
+
type PasswordProvider,
|
|
66
|
+
} from "./shared.ts";
|
|
67
|
+
|
|
68
|
+
/** Explicit, fully injected checkpoint dependencies. Every field is required; no defaults exist. */
|
|
69
|
+
export interface CheckpointFlowInput {
|
|
70
|
+
/** Explicit project scope for collection; the collector re-validates it and never resolves "current". */
|
|
71
|
+
readonly projectID: string;
|
|
72
|
+
/** Current project directory used by the P-15 path policy (default `.sessionport/` location). */
|
|
73
|
+
readonly projectDirectory: string;
|
|
74
|
+
/** Explicit collection bounds; no defaults are applied (O-03). */
|
|
75
|
+
readonly policy: SessionCollectionPolicy;
|
|
76
|
+
/** Caller-supplied crypto factory; no production factory or profile is created here. */
|
|
77
|
+
readonly cryptoFactory: CheckpointCryptoFactory;
|
|
78
|
+
/** Explicit allow-listed policy for `factory.create`; never taken from an artifact. */
|
|
79
|
+
readonly cryptoPolicy: CryptoPolicy;
|
|
80
|
+
/** Injected `resolveCheckpointOutputPath` (P-15); the real function is passed by the product wiring. */
|
|
81
|
+
readonly resolveCheckpointOutputPath: (options: ResolveCheckpointOutputPathOptions) => Promise<string>;
|
|
82
|
+
/** Injected `writeEncryptedArtifact`; receives already-sealed ciphertext bytes only. */
|
|
83
|
+
readonly writeEncryptedArtifact: (targetPath: string, source: ByteSource) => Promise<number>;
|
|
84
|
+
/** Interactive password provider; called once per run, `undefined` means canceled. */
|
|
85
|
+
readonly requestPassword: PasswordProvider;
|
|
86
|
+
/** Optional explicit output-path override (P-15); omitted uses the default location. */
|
|
87
|
+
readonly explicitOutputPath?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Low-level checkpoint outcome. No O-15 report format is implied. */
|
|
91
|
+
export type CheckpointOutcome =
|
|
92
|
+
/** P-04: the explicit project scope returned zero sessions; nothing was prompted, resolved, or written. */
|
|
93
|
+
| { readonly status: "no-sessions" }
|
|
94
|
+
/** The user canceled the password prompt; nothing was resolved, encrypted, or written. */
|
|
95
|
+
| { readonly status: "canceled" }
|
|
96
|
+
/** The sealed artifact was written; the facts below are raw observations, not a UX report. */
|
|
97
|
+
| {
|
|
98
|
+
readonly status: "written";
|
|
99
|
+
readonly artifactPath: string;
|
|
100
|
+
readonly sessionCount: number;
|
|
101
|
+
readonly encryptedBytes: number;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Runs one checkpoint to a completed encrypted artifact or to a clear
|
|
106
|
+
* no-sessions/canceled outcome. Errors propagate unchanged; see the module
|
|
107
|
+
* comment for the exact sequence and policy boundaries.
|
|
108
|
+
*/
|
|
109
|
+
export async function runCheckpointFlow(
|
|
110
|
+
port: SessionReadPort,
|
|
111
|
+
input: CheckpointFlowInput,
|
|
112
|
+
): Promise<CheckpointOutcome> {
|
|
113
|
+
if (!isRecordValue(input)) {
|
|
114
|
+
throw new TypeError(
|
|
115
|
+
"runCheckpointFlow requires an input object with explicit projectID, projectDirectory, policy, cryptoFactory, cryptoPolicy, resolveCheckpointOutputPath, writeEncryptedArtifact, and requestPassword",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
requireCallable(input.requestPassword, "requestPassword");
|
|
119
|
+
requireCallable(input.resolveCheckpointOutputPath, "resolveCheckpointOutputPath");
|
|
120
|
+
requireCallable(input.writeEncryptedArtifact, "writeEncryptedArtifact");
|
|
121
|
+
if (!isRecordValue(input.cryptoFactory) || typeof input.cryptoFactory.create !== "function") {
|
|
122
|
+
throw new TypeError("runCheckpointFlow requires a CheckpointCryptoFactory with a create(policy) function");
|
|
123
|
+
}
|
|
124
|
+
const projectID = requireNonEmptyString(input.projectID, "projectID");
|
|
125
|
+
requireNonEmptyString(input.projectDirectory, "projectDirectory");
|
|
126
|
+
|
|
127
|
+
const payload = await collectCheckpointPayload(port, { projectID, policy: input.policy });
|
|
128
|
+
if (payload.sessions.length === 0) {
|
|
129
|
+
// P-04: no empty checkpoints. Nothing is prompted, resolved, encrypted, or written.
|
|
130
|
+
return { status: "no-sessions" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const password = await input.requestPassword("checkpoint");
|
|
134
|
+
if (password === undefined) {
|
|
135
|
+
return { status: "canceled" };
|
|
136
|
+
}
|
|
137
|
+
if (!(password instanceof Uint8Array)) {
|
|
138
|
+
throw new TypeError("The checkpoint password provider must resolve with Uint8Array bytes or undefined");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const artifactPath = await input.resolveCheckpointOutputPath({
|
|
143
|
+
projectDirectory: input.projectDirectory,
|
|
144
|
+
...(input.explicitOutputPath !== undefined ? { explicitPath: input.explicitOutputPath } : {}),
|
|
145
|
+
});
|
|
146
|
+
const crypto = await input.cryptoFactory.create(input.cryptoPolicy);
|
|
147
|
+
// `serializeCheckpointPayload` returns bytes; `seal` consumes a ByteSource,
|
|
148
|
+
// so the serialized payload is handed over as a one-shot borrowed chunk.
|
|
149
|
+
const sealed = await crypto.seal(asOneShotByteSource(serializeCheckpointPayload(payload)), password);
|
|
150
|
+
const encryptedBytes = await input.writeEncryptedArtifact(artifactPath, sealed);
|
|
151
|
+
if (typeof encryptedBytes !== "number" || !Number.isSafeInteger(encryptedBytes) || encryptedBytes < 0) {
|
|
152
|
+
throw new TypeError("writeEncryptedArtifact must resolve with the exact non-negative integer byte count written");
|
|
153
|
+
}
|
|
154
|
+
return { status: "written", artifactPath, sessionCount: payload.sessions.length, encryptedBytes };
|
|
155
|
+
} finally {
|
|
156
|
+
// Best-effort only (A-07): the password is zeroed after the sealed
|
|
157
|
+
// iterable has been consumed (or the write failed), never before.
|
|
158
|
+
zeroPasswordBestEffort(password);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionPort V1 TUI orchestration (Phase 4, bounded non-visual lane; `fix-57`).
|
|
3
|
+
*
|
|
4
|
+
* Public surface:
|
|
5
|
+
* - `runCheckpointFlow(port, input)` → collect, prompt, resolve the P-15 path,
|
|
6
|
+
* seal, and write one encrypted artifact; returns only `no-sessions` /
|
|
7
|
+
* `canceled` / `written` low-level facts.
|
|
8
|
+
* - `runRestorePreflight(client, input)` → O-12 capability probe, one password
|
|
9
|
+
* attempt, authenticated strict-parse of the artifact, destination snapshot,
|
|
10
|
+
* the required bounded metadata-only cross-project session-ID occupancy
|
|
11
|
+
* inventory (owner-approved 2026-10-14 amendment; after auth/parse and the
|
|
12
|
+
* destination read, before planning), and a completed `planRestore`; returns
|
|
13
|
+
* the plan plus the caller's explicit target directory.
|
|
14
|
+
* - `runRestorePreflightWithPasswordAttempts(client, input)` → the
|
|
15
|
+
* owner-decided three-attempt restore password loop around
|
|
16
|
+
* `runRestorePreflight`; retries only the stable `authentication-failed`
|
|
17
|
+
* code and returns a terminal `authentication-failed` outcome after the
|
|
18
|
+
* permitted attempts instead of rethrowing the raw crypto error.
|
|
19
|
+
* - `runRestoreExecution(input)` → sequential parent-first execution of one
|
|
20
|
+
* completed `RestorePlan` through the injected `SessionImportAdapter` with
|
|
21
|
+
* truthful per-session outcomes (imported / skipped / failed / blocked /
|
|
22
|
+
* not-attempted), no retries, independent-branch continuation, blocked
|
|
23
|
+
* descendants of a failed/blocked/unattempted parent, and the caller's
|
|
24
|
+
* explicit target on every import (O-06 handling policy; the final report
|
|
25
|
+
* format stays OPEN as O-15).
|
|
26
|
+
*
|
|
27
|
+
* This lane performs no command registration, no real host calls, and no
|
|
28
|
+
* production crypto configuration. All effects are injected; tests use fake
|
|
29
|
+
* ports, fake clients, fake importers, fake crypto, and fake store functions
|
|
30
|
+
* only.
|
|
31
|
+
*
|
|
32
|
+
* OPEN decision boundaries preserved here: O-01/O-02/O-03 (no production
|
|
33
|
+
* profile, format, or numeric cap), O-06 (owner handling policy implemented;
|
|
34
|
+
* runtime partial-import behavior remains unvalidated), O-10 (child-session
|
|
35
|
+
* enumeration: the checkpoint flow adds none), O-11 (damage policy), O-15
|
|
36
|
+
* (final error taxonomy/report format: none is produced here), O-16 (the
|
|
37
|
+
* restore caller supplies an explicit target). No runtime, compatibility, or
|
|
38
|
+
* security claim follows from this module.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
export { runCheckpointFlow } from "./checkpoint.ts";
|
|
42
|
+
export type { CheckpointFlowInput, CheckpointOutcome } from "./checkpoint.ts";
|
|
43
|
+
export { runRestorePreflight } from "./restore-preflight.ts";
|
|
44
|
+
export type { RestorePreflightInput, RestorePreflightOutcome } from "./restore-preflight.ts";
|
|
45
|
+
export { isAuthenticationFailure, RESTORE_PASSWORD_ATTEMPT_LIMIT, runRestorePreflightWithPasswordAttempts } from "./restore-auth.ts";
|
|
46
|
+
export type { RestoreAuthenticatedPreflightOutcome } from "./restore-auth.ts";
|
|
47
|
+
export { runRestoreExecution } from "./restore-execution.ts";
|
|
48
|
+
export type {
|
|
49
|
+
RestoreExecutionInput,
|
|
50
|
+
RestoreExecutionResult,
|
|
51
|
+
RestoreExecutionSummary,
|
|
52
|
+
RestoreSessionExecutionStatus,
|
|
53
|
+
RestoreSessionOutcome,
|
|
54
|
+
} from "./restore-execution.ts";
|
|
55
|
+
export type { PasswordProvider, PasswordPurpose } from "./shared.ts";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-decided restore password-attempt loop (Phase 2 experimental wiring).
|
|
3
|
+
*
|
|
4
|
+
* `runRestorePreflightWithPasswordAttempts` wraps the injected
|
|
5
|
+
* `runRestorePreflight` (Phase 4 orchestration) with the owner decision
|
|
6
|
+
* recorded in O-07/P-13 (2026-10-10): a restore permits **three failed
|
|
7
|
+
* password attempts per command run**.
|
|
8
|
+
*
|
|
9
|
+
* Exact semantics:
|
|
10
|
+
* - Each attempt calls `runRestorePreflight` exactly once, which prompts for
|
|
11
|
+
* the password through the injected provider and performs the full
|
|
12
|
+
* capability probe, lazy artifact read, authentication, strict parse,
|
|
13
|
+
* destination snapshot, the required bounded metadata-only cross-project
|
|
14
|
+
* session-ID occupancy inventory (owner-approved 2026-10-14 amendment), and
|
|
15
|
+
* the `planRestore` preflight.
|
|
16
|
+
* - Only an error carrying the stable crypto code `"authentication-failed"` is
|
|
17
|
+
* retried. The classification is structural (`error.code`), never based on
|
|
18
|
+
* message text, so no raw exception detail is inspected, exposed, or logged.
|
|
19
|
+
* - After the final permitted attempt, the outcome is a truthful
|
|
20
|
+
* `{ status: "authentication-failed" }`; the raw error is not rethrown, so
|
|
21
|
+
* the caller reports generic copy. This deliberately does not distinguish a
|
|
22
|
+
* wrong password from a damaged artifact.
|
|
23
|
+
* - Every other error (truncated/tampered artifact, invalid UTF-8/JSON,
|
|
24
|
+
* destination/planner failure, missing capability, provider failure)
|
|
25
|
+
* propagates unchanged on its first occurrence; nothing is retried.
|
|
26
|
+
* - `canceled` and `capability-unavailable` outcomes pass through unchanged and
|
|
27
|
+
* are never retried.
|
|
28
|
+
*
|
|
29
|
+
* Password ownership stays with `runRestorePreflight`: each attempt's resolved
|
|
30
|
+
* bytes are best-effort zeroed in its `finally` block; this wrapper never
|
|
31
|
+
* retains a password. No import is performed here; the caller owns any later
|
|
32
|
+
* `runRestoreExecution`.
|
|
33
|
+
*
|
|
34
|
+
* Evidence boundary: fake clients, fake crypto, fake artifact sources, fake
|
|
35
|
+
* destination callbacks, and a fake cross-project inventory callback only. No
|
|
36
|
+
* real host call, artifact read, KDF, or import is performed by this lane, and
|
|
37
|
+
* no runtime or compatibility claim follows.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
runRestorePreflight,
|
|
42
|
+
type RestorePreflightInput,
|
|
43
|
+
type RestorePreflightOutcome,
|
|
44
|
+
} from "./restore-preflight.ts";
|
|
45
|
+
import { isRecordValue } from "./shared.ts";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Owner-decided per-command-run restore attempt limit (O-07/P-13,
|
|
49
|
+
* 2026-10-10): three failed password attempts, then stop.
|
|
50
|
+
*/
|
|
51
|
+
export const RESTORE_PASSWORD_ATTEMPT_LIMIT = 3;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Restore-preflight outcome including the terminal three-attempt
|
|
55
|
+
* authentication failure. The final case is returned instead of rethrowing the
|
|
56
|
+
* raw crypto error so callers can report generic copy.
|
|
57
|
+
*/
|
|
58
|
+
export type RestoreAuthenticatedPreflightOutcome =
|
|
59
|
+
| RestorePreflightOutcome
|
|
60
|
+
| { readonly status: "authentication-failed" };
|
|
61
|
+
|
|
62
|
+
/** True only for the stable crypto `authentication-failed` code; never inspects message text. */
|
|
63
|
+
export function isAuthenticationFailure(error: unknown): boolean {
|
|
64
|
+
return isRecordValue(error) && error["code"] === "authentication-failed";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Runs the restore preflight with up to `maxAttempts` password attempts
|
|
69
|
+
* (default: the owner-decided three). See the module comment for the exact
|
|
70
|
+
* retry/classification policy.
|
|
71
|
+
*/
|
|
72
|
+
export async function runRestorePreflightWithPasswordAttempts(
|
|
73
|
+
client: unknown,
|
|
74
|
+
input: RestorePreflightInput,
|
|
75
|
+
maxAttempts: number = RESTORE_PASSWORD_ATTEMPT_LIMIT,
|
|
76
|
+
): Promise<RestoreAuthenticatedPreflightOutcome> {
|
|
77
|
+
if (typeof maxAttempts !== "number" || !Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
|
|
78
|
+
throw new TypeError("maxAttempts must be an explicit positive safe integer");
|
|
79
|
+
}
|
|
80
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
81
|
+
try {
|
|
82
|
+
return await runRestorePreflight(client, input);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (!isAuthenticationFailure(error)) throw error;
|
|
85
|
+
if (attempt === maxAttempts) {
|
|
86
|
+
// Give up after the permitted attempts without disclosing the raw
|
|
87
|
+
// crypto error; the caller reports generic copy.
|
|
88
|
+
return { status: "authentication-failed" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Unreachable: the loop returns or throws on every iteration.
|
|
93
|
+
return { status: "authentication-failed" };
|
|
94
|
+
}
|