@yefengr/remote-pi 0.9.8 → 0.9.9
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/README.md +1 -0
- package/dist/pairing/identity-keyring.d.ts +22 -0
- package/dist/pairing/identity-keyring.js +94 -0
- package/dist/pairing/identity-keyring.js.map +1 -0
- package/dist/pairing/identity-lock.d.ts +14 -0
- package/dist/pairing/identity-lock.js +116 -0
- package/dist/pairing/identity-lock.js.map +1 -0
- package/dist/pairing/qr.d.ts +2 -3
- package/dist/pairing/qr.js +2 -3
- package/dist/pairing/qr.js.map +1 -1
- package/dist/pairing/storage.d.ts +19 -61
- package/dist/pairing/storage.js +196 -314
- package/dist/pairing/storage.js.map +1 -1
- package/dist/vendor/protocol/constants.d.ts +3 -0
- package/dist/vendor/protocol/constants.js +3 -0
- package/dist/vendor/protocol/outer/index.d.ts +1 -1
- package/dist/vendor/protocol/outer/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ Only `http://` and `https://` are accepted at the command boundary; WebSocket co
|
|
|
80
80
|
- Pairing and revocation update the Relay endpoint ACL with `authorized_owner_ids`.
|
|
81
81
|
- Relay loss enters reconnecting state; the Extension does not restart Pi to recover.
|
|
82
82
|
- Device private keys, pairing tokens, encrypted payloads, and message bodies are not logged.
|
|
83
|
+
- Concurrent Pi processes coordinate device identity initialization through a local lock. If initialization is interrupted, follow the [identity storage and lock recovery rules](../docs/reference/protocol/pairing.md#host); do not delete identity or pairing data to retry.
|
|
83
84
|
|
|
84
85
|
## Migration from older releases
|
|
85
86
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IDENTITY_LOCK_RELEASE_AFTER, type IdentityLockDeferredRelease } from "./identity-lock.js";
|
|
2
|
+
export interface KeyStoreBackend {
|
|
3
|
+
read(service: string, account: string): Promise<string | undefined>;
|
|
4
|
+
write(service: string, account: string, value: string): Promise<void>;
|
|
5
|
+
delete(service: string, account: string): Promise<boolean>;
|
|
6
|
+
}
|
|
7
|
+
export declare class KeyringReadTimeoutError extends Error {
|
|
8
|
+
constructor(op: string, timeoutMs: number);
|
|
9
|
+
}
|
|
10
|
+
export declare class KeyringMutationTimeoutError extends Error implements IdentityLockDeferredRelease {
|
|
11
|
+
readonly [IDENTITY_LOCK_RELEASE_AFTER]: Promise<void>;
|
|
12
|
+
constructor(op: string, timeoutMs: number, settled: Promise<void>);
|
|
13
|
+
}
|
|
14
|
+
export declare function _setKeyringOperationTimeoutForTest(timeoutMs: number | null): void;
|
|
15
|
+
export declare function _runKeyringOperationWithTimeoutForTest<T>(operation: Promise<T>, op: string, mutating: boolean, timeoutMs: number): Promise<T>;
|
|
16
|
+
export declare function nativeBindingUnavailable(): boolean;
|
|
17
|
+
export declare function _setNativeBindingErrorForTest(error: unknown): void;
|
|
18
|
+
export declare class NapiKeyringBackend implements KeyStoreBackend {
|
|
19
|
+
read(service: string, account: string): Promise<string | undefined>;
|
|
20
|
+
write(service: string, account: string, value: string): Promise<void>;
|
|
21
|
+
delete(service: string, account: string): Promise<boolean>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { IDENTITY_LOCK_RELEASE_AFTER, } from "./identity-lock.js";
|
|
2
|
+
export class KeyringReadTimeoutError extends Error {
|
|
3
|
+
constructor(op, timeoutMs) {
|
|
4
|
+
super(`keyring ${op} timed out after ${timeoutMs}ms`);
|
|
5
|
+
this.name = "KeyringReadTimeoutError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class KeyringMutationTimeoutError extends Error {
|
|
9
|
+
[IDENTITY_LOCK_RELEASE_AFTER];
|
|
10
|
+
constructor(op, timeoutMs, settled) {
|
|
11
|
+
super(`keyring ${op} timed out after ${timeoutMs}ms; the underlying operation is still pending`);
|
|
12
|
+
this.name = "KeyringMutationTimeoutError";
|
|
13
|
+
this[IDENTITY_LOCK_RELEASE_AFTER] = settled.then(() => undefined, () => undefined);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let keyringOperationTimeoutMs = 3_000;
|
|
17
|
+
export function _setKeyringOperationTimeoutForTest(timeoutMs) {
|
|
18
|
+
keyringOperationTimeoutMs = timeoutMs ?? 3_000;
|
|
19
|
+
}
|
|
20
|
+
export function _runKeyringOperationWithTimeoutForTest(operation, op, mutating, timeoutMs) {
|
|
21
|
+
return withKeyringTimeout(operation, op, mutating, timeoutMs);
|
|
22
|
+
}
|
|
23
|
+
function withKeyringTimeout(operation, op, mutating, timeoutMs = keyringOperationTimeoutMs) {
|
|
24
|
+
const settled = operation.then(() => undefined, () => undefined);
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
reject(mutating
|
|
28
|
+
? new KeyringMutationTimeoutError(op, timeoutMs, settled)
|
|
29
|
+
: new KeyringReadTimeoutError(op, timeoutMs));
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
operation.then((value) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolve(value);
|
|
34
|
+
}, (error) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
reject(error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
let asyncEntryCtor = null;
|
|
41
|
+
let nativeBindingError = null;
|
|
42
|
+
async function loadAsyncEntry() {
|
|
43
|
+
if (asyncEntryCtor)
|
|
44
|
+
return asyncEntryCtor;
|
|
45
|
+
if (nativeBindingError)
|
|
46
|
+
throw nativeBindingError;
|
|
47
|
+
try {
|
|
48
|
+
const mod = await import("@napi-rs/keyring");
|
|
49
|
+
asyncEntryCtor = mod.AsyncEntry;
|
|
50
|
+
return asyncEntryCtor;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
nativeBindingError = error;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function nativeBindingUnavailable() {
|
|
58
|
+
return nativeBindingError !== null;
|
|
59
|
+
}
|
|
60
|
+
export function _setNativeBindingErrorForTest(error) {
|
|
61
|
+
asyncEntryCtor = null;
|
|
62
|
+
nativeBindingError = error;
|
|
63
|
+
}
|
|
64
|
+
export class NapiKeyringBackend {
|
|
65
|
+
async read(service, account) {
|
|
66
|
+
const AsyncEntry = await loadAsyncEntry();
|
|
67
|
+
const entry = new AsyncEntry(service, account);
|
|
68
|
+
return withKeyringTimeout(entry.getPassword(), `read(${service})`, false);
|
|
69
|
+
}
|
|
70
|
+
async write(service, account, value) {
|
|
71
|
+
const AsyncEntry = await loadAsyncEntry();
|
|
72
|
+
const entry = new AsyncEntry(service, account);
|
|
73
|
+
await withKeyringTimeout(entry.setPassword(value), `write(${service})`, true);
|
|
74
|
+
}
|
|
75
|
+
async delete(service, account) {
|
|
76
|
+
let entry;
|
|
77
|
+
try {
|
|
78
|
+
const AsyncEntry = await loadAsyncEntry();
|
|
79
|
+
entry = new AsyncEntry(service, account);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return await withKeyringTimeout(entry.deleteCredential(), `delete(${service})`, true);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof KeyringMutationTimeoutError)
|
|
89
|
+
throw error;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=identity-keyring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-keyring.js","sourceRoot":"","sources":["../../src/pairing/identity-keyring.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const IDENTITY_LOCK_RELEASE_AFTER: unique symbol;
|
|
2
|
+
export interface IdentityLockDeferredRelease {
|
|
3
|
+
readonly [IDENTITY_LOCK_RELEASE_AFTER]: Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface IdentityLockOptions {
|
|
6
|
+
readonly waitTimeoutMs?: number;
|
|
7
|
+
readonly pollIntervalMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class IdentityLockTimeoutError extends Error {
|
|
10
|
+
readonly lockPath: string;
|
|
11
|
+
constructor(lockPath: string, waitTimeoutMs: number);
|
|
12
|
+
}
|
|
13
|
+
export declare function ensurePrivateIdentityDirectory(directory: string): Promise<void>;
|
|
14
|
+
export declare function withIdentityLock<T>(directory: string, operation: () => Promise<T>, options?: IdentityLockOptions): Promise<T>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export const IDENTITY_LOCK_RELEASE_AFTER = Symbol("identity-lock-release-after");
|
|
5
|
+
export class IdentityLockTimeoutError extends Error {
|
|
6
|
+
lockPath;
|
|
7
|
+
constructor(lockPath, waitTimeoutMs) {
|
|
8
|
+
super(`Identity initialization lock remained held for ${waitTimeoutMs}ms at ${lockPath}. ` +
|
|
9
|
+
"Another process may still be initializing the identity, or a previous process may " +
|
|
10
|
+
"have exited unexpectedly. The lock is never removed automatically. Confirm that no " +
|
|
11
|
+
"identity initialization or keyring operation is still running before removing it manually.");
|
|
12
|
+
this.name = "IdentityLockTimeoutError";
|
|
13
|
+
this.lockPath = lockPath;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function isNodeErrorWithCode(error, code) {
|
|
17
|
+
return typeof error === "object" && error !== null && "code" in error &&
|
|
18
|
+
error.code === code;
|
|
19
|
+
}
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
export async function ensurePrivateIdentityDirectory(directory) {
|
|
24
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
await chmod(directory, 0o700);
|
|
26
|
+
}
|
|
27
|
+
async function tryAcquire(lockPath) {
|
|
28
|
+
let handle;
|
|
29
|
+
try {
|
|
30
|
+
handle = await open(lockPath, "wx", 0o600);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (isNodeErrorWithCode(error, "EEXIST"))
|
|
34
|
+
return null;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const token = randomUUID();
|
|
38
|
+
try {
|
|
39
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, token, createdAt: new Date().toISOString() }));
|
|
40
|
+
await handle.sync();
|
|
41
|
+
return { handle, path: lockPath, token };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
await handle.close().catch(() => undefined);
|
|
45
|
+
await unlink(lockPath).catch(() => undefined);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function acquireIdentityLock(directory, options) {
|
|
50
|
+
await ensurePrivateIdentityDirectory(directory);
|
|
51
|
+
const lockPath = join(directory, "identity.lock");
|
|
52
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? 15_000;
|
|
53
|
+
const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 50);
|
|
54
|
+
const deadline = Date.now() + Math.max(0, waitTimeoutMs);
|
|
55
|
+
while (true) {
|
|
56
|
+
const held = await tryAcquire(lockPath);
|
|
57
|
+
if (held)
|
|
58
|
+
return held;
|
|
59
|
+
const remainingMs = deadline - Date.now();
|
|
60
|
+
if (remainingMs <= 0)
|
|
61
|
+
throw new IdentityLockTimeoutError(lockPath, waitTimeoutMs);
|
|
62
|
+
await sleep(Math.min(pollIntervalMs, remainingMs));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function releaseIdentityLock(lock) {
|
|
66
|
+
try {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await readFile(lock.path, "utf8");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (isNodeErrorWithCode(error, "ENOENT"))
|
|
73
|
+
return;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
let token;
|
|
77
|
+
try {
|
|
78
|
+
token = JSON.parse(raw).token;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (token !== lock.token)
|
|
84
|
+
return;
|
|
85
|
+
await unlink(lock.path);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await lock.handle.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function deferredRelease(error) {
|
|
92
|
+
if (typeof error !== "object" || error === null)
|
|
93
|
+
return null;
|
|
94
|
+
const settled = error[IDENTITY_LOCK_RELEASE_AFTER];
|
|
95
|
+
return settled instanceof Promise ? settled : null;
|
|
96
|
+
}
|
|
97
|
+
export async function withIdentityLock(directory, operation, options = {}) {
|
|
98
|
+
const lock = await acquireIdentityLock(directory, options);
|
|
99
|
+
let releaseWasDeferred = false;
|
|
100
|
+
try {
|
|
101
|
+
return await operation();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const settled = deferredRelease(error);
|
|
105
|
+
if (settled) {
|
|
106
|
+
releaseWasDeferred = true;
|
|
107
|
+
void settled.then(() => releaseIdentityLock(lock), () => releaseIdentityLock(lock)).catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (!releaseWasDeferred)
|
|
113
|
+
await releaseIdentityLock(lock);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=identity-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-lock.js","sourceRoot":"","sources":["../../src/pairing/identity-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAmB,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC,6BAA6B,CAAC,CAAC;AAWjF,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACxC,QAAQ,CAAS;IAE1B,YAAY,QAAgB,EAAE,aAAqB;QACjD,KAAK,CACH,kDAAkD,aAAa,SAAS,QAAQ,IAAI;YACpF,oFAAoF;YACpF,qFAAqF;YACrF,4FAA4F,CAC7F,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;QACvC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAQD,SAAS,mBAAmB,CAAC,KAAc,EAAE,IAAY;IACvD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK;QAClE,KAA4B,CAAC,IAAI,KAAK,IAAI,CAAC;AAChD,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,SAAiB;IACpE,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,MAAM,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAgB;IACxC,IAAI,MAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QACtD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC;QACzG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC3C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC9C,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,SAAiB,EACjB,OAA4B;IAE5B,MAAM,8BAA8B,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAClD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,MAAM,CAAC;IACtD,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;IAEzD,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QAEtB,MAAM,WAAW,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC1C,IAAI,WAAW,IAAI,CAAC;YAAE,MAAM,IAAI,wBAAwB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;QAClF,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC;IACrD,CAAC;AACH,CAAC;AAED,KAAK,UAAU,mBAAmB,CAAC,IAAsB;IACvD,IAAI,CAAC;QACH,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC;gBAAE,OAAO;YACjD,MAAM,KAAK,CAAC;QACd,CAAC;QAED,IAAI,KAAc,CAAC;QACnB,IAAI,CAAC;YACH,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC,KAAK,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK;YAAE,OAAO;QACjC,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,OAAO,GAAI,KAA8C,CAAC,2BAA2B,CAAC,CAAC;IAC7F,OAAO,OAAO,YAAY,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,SAAiB,EACjB,SAA2B,EAC3B,UAA+B,EAAE;IAEjC,MAAM,IAAI,GAAG,MAAM,mBAAmB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC3D,IAAI,kBAAkB,GAAG,KAAK,CAAC;IAC/B,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,OAAO,EAAE,CAAC;YACZ,kBAAkB,GAAG,IAAI,CAAC;YAC1B,KAAK,OAAO,CAAC,IAAI,CACf,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAC/B,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAChC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC3B,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,kBAAkB;YAAE,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC"}
|
package/dist/pairing/qr.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export declare const PAIR_TTL_MAX_MS = 600000;
|
|
1
|
+
import { PAIRING_INVITE_TTL_MS, PAIR_TTL_MAX_MS, PAIR_TTL_MIN_MS } from "../vendor/protocol/outer/index.js";
|
|
2
|
+
export { PAIRING_INVITE_TTL_MS, PAIR_TTL_MAX_MS, PAIR_TTL_MIN_MS };
|
|
4
3
|
export declare const PAIRING_CODE_LENGTH = 8;
|
|
5
4
|
export declare const PAIRING_CODE_PATTERN: RegExp;
|
|
6
5
|
export declare function clampPairTtlMs(ttlMs: number): number;
|
package/dist/pairing/qr.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { PAIRING_INVITE_TTL_MS, PAIR_TTL_MAX_MS, PAIR_TTL_MIN_MS } from "../vendor/protocol/outer/index.js";
|
|
2
3
|
import qrTerminal from "qrcode-terminal";
|
|
3
|
-
export
|
|
4
|
-
export const PAIR_TTL_MIN_MS = 10_000;
|
|
5
|
-
export const PAIR_TTL_MAX_MS = 600_000;
|
|
4
|
+
export { PAIRING_INVITE_TTL_MS, PAIR_TTL_MAX_MS, PAIR_TTL_MIN_MS };
|
|
6
5
|
export const PAIRING_CODE_LENGTH = 8;
|
|
7
6
|
export const PAIRING_CODE_PATTERN = /^[0-9A-HJKMNP-TV-Z]{8}$/;
|
|
8
7
|
const CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
package/dist/pairing/qr.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,
|
|
1
|
+
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,78 +1,36 @@
|
|
|
1
1
|
import { type Ed25519Keypair } from "./crypto.js";
|
|
2
|
+
import { type KeyStoreBackend } from "./identity-keyring.js";
|
|
2
3
|
export { addPeer, listPeers, listOwnerPubkeys, snapshotOwnerPubkeys, conditionalRemovePeer, conditionalRollbackPeer, removePeer, } from "./owner_storage.js";
|
|
3
4
|
export type { PeerRecord, OwnerStorageToken, OwnerStorageSnapshotRecord, ConditionalPeerRemoval, PeerWriteReceipt, ConditionalPeerRollback, } from "./owner_storage.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* would break existing pairing — the caller surfaces this so the user can
|
|
8
|
-
* unlock the keychain and retry instead of silently re-pairing. */
|
|
5
|
+
export { _setKeyringOperationTimeoutForTest, _setNativeBindingErrorForTest, KeyringMutationTimeoutError, KeyringReadTimeoutError, } from "./identity-keyring.js";
|
|
6
|
+
export type { KeyStoreBackend } from "./identity-keyring.js";
|
|
7
|
+
export { IdentityLockTimeoutError } from "./identity-lock.js";
|
|
9
8
|
export declare class KeyringUnavailableError extends Error {
|
|
10
9
|
constructor(cause: unknown);
|
|
11
10
|
}
|
|
12
|
-
/** Raised when no identity can be resolved (keyring unreadable, no identity
|
|
13
|
-
* file) BUT `peers.json` already lists paired devices. Minting a fresh key
|
|
14
|
-
* here would make those existing pairings unusable — see issues #95 / #69. */
|
|
15
11
|
export declare class PairedIdentityMissingError extends Error {
|
|
16
12
|
constructor(pairedCount: number, cause: unknown);
|
|
17
13
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*/
|
|
28
|
-
export interface KeyStoreBackend {
|
|
29
|
-
read(service: string, account: string): Promise<string | undefined>;
|
|
30
|
-
write(service: string, account: string, value: string): Promise<void>;
|
|
31
|
-
delete(service: string, account: string): Promise<boolean>;
|
|
14
|
+
type IdentityFileErrorCategory = "read_failed" | "invalid_format";
|
|
15
|
+
export declare class IdentityFileError extends Error {
|
|
16
|
+
readonly identityPath: string;
|
|
17
|
+
readonly category: IdentityFileErrorCategory;
|
|
18
|
+
constructor(identityPath: string, category: IdentityFileErrorCategory);
|
|
19
|
+
}
|
|
20
|
+
export declare class KeyringIdentityError extends Error {
|
|
21
|
+
readonly service: string;
|
|
22
|
+
constructor(service: string);
|
|
32
23
|
}
|
|
33
|
-
|
|
34
|
-
* the Bun/no-native-binding branch is reachable without a Bun host. */
|
|
35
|
-
export declare function _setNativeBindingErrorForTest(err: unknown): void;
|
|
36
|
-
/** Test-only: swap (or clear with `null`) the keyring backend. */
|
|
37
|
-
export declare function _setKeyStoreBackendForTest(backend: KeyStoreBackend | null): void;
|
|
38
|
-
/** Test-only: force `_keyringExpectedAvailable()` (so a darwin test host can
|
|
39
|
-
* exercise the Linux/headless branch and vice-versa). `null` restores the
|
|
40
|
-
* real platform check. */
|
|
24
|
+
export declare function _setKeyStoreBackendForTest(value: KeyStoreBackend | null): void;
|
|
41
25
|
export declare function _setKeyringExpectedForTest(value: boolean | null): void;
|
|
42
|
-
/** Test-only: shrink retry attempts/delay so the persistent-failure path is
|
|
43
|
-
* fast. `null`/omitted restores defaults. */
|
|
44
26
|
export declare function _setKeyringRetryForTest(attempts: number | null, delayMs?: number): void;
|
|
27
|
+
export declare function _setIdentityLockTimingForTest(waitTimeoutMs: number | null, pollIntervalMs?: number): void;
|
|
45
28
|
/**
|
|
46
|
-
* Returns the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* the keyring. A file identity is only ever written by the headless/
|
|
50
|
-
* degraded fallback (step 4) or an explicit `REMOTE_PI_ALLOW_FILE_IDENTITY`
|
|
51
|
-
* opt-in, so its mere presence means this machine established its identity
|
|
52
|
-
* as a file and the browser PWA paired against THAT pubkey. If the
|
|
53
|
-
* platform keyring later becomes readable (D-Bus/libsecret installed, a
|
|
54
|
-
* desktop session, or a stale/other entry from another install), reading
|
|
55
|
-
* it first would mask the file identity — returning a DIFFERENT key, or
|
|
56
|
-
* (when the keyring is empty) minting a fresh one and persisting it —
|
|
57
|
-
* silently breaking the existing pairing. So when both exist, file wins.
|
|
58
|
-
* 2. New keyring service `dev.remotepi.pi` (read retried — a transiently
|
|
59
|
-
* locked Keychain throws; we don't treat that as "no key")
|
|
60
|
-
* 3. Old keyring service `dev.remotepi.mac` (migrate → step 2, delete old)
|
|
61
|
-
* 4. Generate a fresh keypair, BUT only when it's safe to: either both
|
|
62
|
-
* keyring reads succeeded and returned nothing (genuine first run), or
|
|
63
|
-
* the keyring is genuinely unavailable on a platform without a core one
|
|
64
|
-
* (headless Linux → a file identity is minted here). On macOS/Windows a
|
|
65
|
-
* persistent read failure with no file identity throws
|
|
66
|
-
* `KeyringUnavailableError` instead of minting a new key — generating
|
|
67
|
-
* there silently breaks existing pairing (the "lost pairing after idle"
|
|
68
|
-
* bug). `REMOTE_PI_ALLOW_FILE_IDENTITY=1` opts back into a file identity
|
|
69
|
-
* for headless macOS/Windows hosts.
|
|
70
|
-
*
|
|
71
|
-
* Idempotent: subsequent calls return the same identity. The migration
|
|
72
|
-
* runs at most once per machine (the old entry is deleted after copy).
|
|
29
|
+
* Returns the stable Host identity. Existing file identity wins; otherwise all
|
|
30
|
+
* keyring reads, migration, generation, and persistence run under the fixed
|
|
31
|
+
* cross-process identity lock. Every source is read again after lock acquisition.
|
|
73
32
|
*/
|
|
74
33
|
export declare function getOrCreateEd25519Keypair(): Promise<Ed25519Keypair>;
|
|
75
|
-
/** Test-only: expose the identity-file path so tests can clean it. */
|
|
76
34
|
export declare const _IDENTITY_FILE_FOR_TEST: string;
|
|
77
|
-
|
|
35
|
+
export declare const _IDENTITY_LOCK_FILE_FOR_TEST: string;
|
|
78
36
|
export declare const _unlinkIdentityFileForTest: () => Promise<void>;
|
package/dist/pairing/storage.js
CHANGED
|
@@ -1,391 +1,273 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, open, readFile, rename, unlink } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { generateEd25519Keypair } from "./crypto.js";
|
|
6
|
+
import { ensurePrivateIdentityDirectory, withIdentityLock, } from "./identity-lock.js";
|
|
7
|
+
import { NapiKeyringBackend, nativeBindingUnavailable, } from "./identity-keyring.js";
|
|
5
8
|
import { listPeers } from "./owner_storage.js";
|
|
6
9
|
export { addPeer, listPeers, listOwnerPubkeys, snapshotOwnerPubkeys, conditionalRemovePeer, conditionalRollbackPeer, removePeer, } from "./owner_storage.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* via `@napi-rs/keyring` (Keychain on macOS, libsecret on Linux desktop,
|
|
12
|
-
* Credential Manager on Windows — DPAPI-backed). When the keyring is
|
|
13
|
-
* unavailable (headless Linux without a D-Bus session, Docker containers,
|
|
14
|
-
* VPS without GNOME Keyring/KWallet running) we fall back to a
|
|
15
|
-
* file-backed store at `~/.pi/remote/identity.json` with `0o600`
|
|
16
|
-
* permissions and the parent dir at `0o700`.
|
|
17
|
-
*
|
|
18
|
-
* **Migration**: previous builds used `keytar` against service
|
|
19
|
-
* `dev.remotepi.mac`. This module reads from the old service if the new
|
|
20
|
-
* service is empty, copies the entry to the new service `dev.remotepi.pi`,
|
|
21
|
-
* and deletes the old one. Both keytar and `@napi-rs/keyring` address the
|
|
22
|
-
* same OS-level credential store on every supported platform, so the read
|
|
23
|
-
* succeeds without keeping the deprecated `keytar` dependency.
|
|
24
|
-
*/
|
|
25
|
-
const NEW_SERVICE = "dev.remotepi.pi"; // platform-neutral
|
|
26
|
-
const OLD_SERVICE = "dev.remotepi.mac"; // legacy keytar service (pre-2026-05-25)
|
|
10
|
+
export { _setKeyringOperationTimeoutForTest, _setNativeBindingErrorForTest, KeyringMutationTimeoutError, KeyringReadTimeoutError, } from "./identity-keyring.js";
|
|
11
|
+
export { IdentityLockTimeoutError } from "./identity-lock.js";
|
|
12
|
+
const NEW_SERVICE = "dev.remotepi.pi";
|
|
13
|
+
const OLD_SERVICE = "dev.remotepi.mac";
|
|
27
14
|
const ACCOUNT = "longterm-ed25519";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* tests via `_setKeyringRetryForTest`. */
|
|
36
|
-
let _keyringReadAttempts = 3;
|
|
37
|
-
let _keyringRetryDelayMs = 300;
|
|
38
|
-
/** Raised when the keyring is unreadable on a platform where it's a core OS
|
|
39
|
-
* service (macOS Keychain, Windows Credential Manager) AND no prior file
|
|
40
|
-
* identity exists. We refuse to generate a NEW identity here because that
|
|
41
|
-
* would break existing pairing — the caller surfaces this so the user can
|
|
42
|
-
* unlock the keychain and retry instead of silently re-pairing. */
|
|
15
|
+
const PI_DIR = join(homedir(), ".pi", "remote");
|
|
16
|
+
const IDENTITY_FILE = join(PI_DIR, "identity.json");
|
|
17
|
+
const IDENTITY_LOCK_FILE = join(PI_DIR, "identity.lock");
|
|
18
|
+
let keyringReadAttempts = 3;
|
|
19
|
+
let keyringRetryDelayMs = 300;
|
|
20
|
+
let identityLockWaitTimeoutMs = 15_000;
|
|
21
|
+
let identityLockPollIntervalMs = 50;
|
|
43
22
|
export class KeyringUnavailableError extends Error {
|
|
44
23
|
constructor(cause) {
|
|
45
24
|
super("Platform keyring is unreadable and no file-backed identity exists. " +
|
|
46
|
-
"Refusing to generate a NEW identity (that would break existing " +
|
|
47
|
-
"
|
|
25
|
+
"Refusing to generate a NEW identity (that would break existing pairing). " +
|
|
26
|
+
"Unlock your keychain / start your secret service and retry. " +
|
|
48
27
|
"Set REMOTE_PI_ALLOW_FILE_IDENTITY=1 to force a file-backed identity. " +
|
|
49
28
|
`Cause: ${String(cause)}`);
|
|
50
29
|
this.name = "KeyringUnavailableError";
|
|
51
30
|
}
|
|
52
31
|
}
|
|
53
|
-
/** Raised when no identity can be resolved (keyring unreadable, no identity
|
|
54
|
-
* file) BUT `peers.json` already lists paired devices. Minting a fresh key
|
|
55
|
-
* here would make those existing pairings unusable — see issues #95 / #69. */
|
|
56
32
|
export class PairedIdentityMissingError extends Error {
|
|
57
33
|
constructor(pairedCount, cause) {
|
|
58
|
-
super(`No identity could be read, but ${pairedCount} device(s) are already ` +
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"your desktop session). Fix the process keyring access, or pin the " +
|
|
63
|
-
"identity by copying the paired keypair to ~/.pi/remote/identity.json " +
|
|
64
|
-
"(0600), which both contexts read first. " +
|
|
34
|
+
super(`No identity could be read, but ${pairedCount} device(s) are already paired. ` +
|
|
35
|
+
"Refusing to generate a NEW identity - that would revoke every paired device. " +
|
|
36
|
+
"Fix this process's keyring access, or pin the established identity to " +
|
|
37
|
+
"~/.pi/remote/identity.json (0600). " +
|
|
65
38
|
`Cause: ${String(cause)}`);
|
|
66
39
|
this.name = "PairedIdentityMissingError";
|
|
67
40
|
}
|
|
68
41
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* a headless / tmux / non-interactive context. A hang never settles, so
|
|
78
|
-
* without this guard the promise never rejects and the retry + file-fallback
|
|
79
|
-
* logic in getOrCreateEd25519Keypair() is unreachable — freezing the entire
|
|
80
|
-
* /remote-pi pair bootstrap. Raising a real error here lets that fallback
|
|
81
|
-
* chain run as designed.
|
|
82
|
-
*/
|
|
83
|
-
const KEYRING_OP_TIMEOUT_MS = 3_000;
|
|
84
|
-
function _withTimeout(p, op, ms = KEYRING_OP_TIMEOUT_MS) {
|
|
85
|
-
return Promise.race([
|
|
86
|
-
p,
|
|
87
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`keyring ${op} timed out after ${ms}ms`)), ms)),
|
|
88
|
-
]);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Lazily loaded `@napi-rs/keyring` binding — issue #113.
|
|
92
|
-
*
|
|
93
|
-
* A STATIC `import { AsyncEntry } from "@napi-rs/keyring"` is evaluated when
|
|
94
|
-
* the extension module is loaded, so a native binding that cannot be resolved
|
|
95
|
-
* takes the WHOLE extension down at load time ("Failed to load extension …:
|
|
96
|
-
* Cannot find native binding"). That happens on a Bun-compiled `pi`: the
|
|
97
|
-
* loader's first branch (`require("./keyring.<triple>.node")`) is fine, but its
|
|
98
|
-
* fallback (`require("@napi-rs/keyring-<triple>")`, a bare package whose `main`
|
|
99
|
-
* IS the .node file) resolves under Node and not under Bun — and the message it
|
|
100
|
-
* prints then blames npm's optional-dependency bug, sending users off to delete
|
|
101
|
-
* node_modules and losing their other pi packages.
|
|
102
|
-
*
|
|
103
|
-
* Loading on first use turns that fatal load error into an ordinary backend
|
|
104
|
-
* failure, which the existing retry + file-identity fallback already handles.
|
|
105
|
-
*/
|
|
106
|
-
let _asyncEntryCtor = null;
|
|
107
|
-
let _nativeBindingError = null;
|
|
108
|
-
async function _loadAsyncEntry() {
|
|
109
|
-
if (_asyncEntryCtor)
|
|
110
|
-
return _asyncEntryCtor;
|
|
111
|
-
if (_nativeBindingError)
|
|
112
|
-
throw _nativeBindingError;
|
|
113
|
-
try {
|
|
114
|
-
const mod = await import("@napi-rs/keyring");
|
|
115
|
-
_asyncEntryCtor = mod.AsyncEntry;
|
|
116
|
-
return _asyncEntryCtor;
|
|
42
|
+
export class IdentityFileError extends Error {
|
|
43
|
+
identityPath;
|
|
44
|
+
category;
|
|
45
|
+
constructor(identityPath, category) {
|
|
46
|
+
super(`Identity file at ${identityPath} is unsafe to use (${category}). Refusing to replace it.`);
|
|
47
|
+
this.name = "IdentityFileError";
|
|
48
|
+
this.identityPath = identityPath;
|
|
49
|
+
this.category = category;
|
|
117
50
|
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
_nativeBindingError = err;
|
|
120
|
-
throw err;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Did the native binding fail to LOAD (as opposed to an operation failing)?
|
|
125
|
-
*
|
|
126
|
-
* A load failure is deterministic and platform-wide: no retry, no unlock, no
|
|
127
|
-
* amount of waiting brings the keyring back in this process. So it must NOT be
|
|
128
|
-
* treated like a transiently locked Keychain — on macOS/Windows that would
|
|
129
|
-
* throw `KeyringUnavailableError` and leave the user with no working path at
|
|
130
|
-
* all. The file-identity fallback (with its loud warning) is the only usable
|
|
131
|
-
* route here, exactly as on headless Linux.
|
|
132
|
-
*/
|
|
133
|
-
function _nativeBindingUnavailable() {
|
|
134
|
-
return _nativeBindingError !== null;
|
|
135
51
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class NapiKeyringBackend {
|
|
143
|
-
async read(service, account) {
|
|
144
|
-
const AsyncEntry = await _loadAsyncEntry();
|
|
145
|
-
const entry = new AsyncEntry(service, account);
|
|
146
|
-
return _withTimeout(entry.getPassword(), `read(${service})`); // undefined on no-entry
|
|
147
|
-
}
|
|
148
|
-
async write(service, account, value) {
|
|
149
|
-
const AsyncEntry = await _loadAsyncEntry();
|
|
150
|
-
const entry = new AsyncEntry(service, account);
|
|
151
|
-
await _withTimeout(entry.setPassword(value), `write(${service})`);
|
|
152
|
-
}
|
|
153
|
-
async delete(service, account) {
|
|
154
|
-
let entry;
|
|
155
|
-
try {
|
|
156
|
-
const AsyncEntry = await _loadAsyncEntry();
|
|
157
|
-
entry = new AsyncEntry(service, account);
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
try {
|
|
163
|
-
return await _withTimeout(entry.deleteCredential(), `delete(${service})`);
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
52
|
+
export class KeyringIdentityError extends Error {
|
|
53
|
+
service;
|
|
54
|
+
constructor(service) {
|
|
55
|
+
super(`Stored identity in platform keyring service ${service} is invalid. Refusing to replace it.`);
|
|
56
|
+
this.name = "KeyringIdentityError";
|
|
57
|
+
this.service = service;
|
|
168
58
|
}
|
|
169
59
|
}
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
60
|
+
let backend = null;
|
|
61
|
+
let keyringExpectedOverride = null;
|
|
62
|
+
function getBackend() {
|
|
63
|
+
if (!backend)
|
|
64
|
+
backend = new NapiKeyringBackend();
|
|
65
|
+
return backend;
|
|
175
66
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
_backend = backend;
|
|
67
|
+
export function _setKeyStoreBackendForTest(value) {
|
|
68
|
+
backend = value;
|
|
179
69
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
* new identity. On Linux/other the secret service may be genuinely absent
|
|
185
|
-
* (headless, no D-Bus), so the documented file fallback applies. Overridable
|
|
186
|
-
* for tests via `_setKeyringExpectedForTest`. */
|
|
187
|
-
let _keyringExpectedOverride = null;
|
|
188
|
-
function _keyringExpectedAvailable() {
|
|
189
|
-
if (_keyringExpectedOverride !== null)
|
|
190
|
-
return _keyringExpectedOverride;
|
|
191
|
-
// Binding never loaded (Bun-built pi, issue #113) → there is no keyring on
|
|
192
|
-
// this platform *for this process*, whatever the OS normally offers.
|
|
193
|
-
if (_nativeBindingUnavailable())
|
|
70
|
+
function keyringExpectedAvailable() {
|
|
71
|
+
if (keyringExpectedOverride !== null)
|
|
72
|
+
return keyringExpectedOverride;
|
|
73
|
+
if (nativeBindingUnavailable())
|
|
194
74
|
return false;
|
|
195
75
|
return process.platform === "darwin" || process.platform === "win32";
|
|
196
76
|
}
|
|
197
|
-
/** Test-only: force `_keyringExpectedAvailable()` (so a darwin test host can
|
|
198
|
-
* exercise the Linux/headless branch and vice-versa). `null` restores the
|
|
199
|
-
* real platform check. */
|
|
200
77
|
export function _setKeyringExpectedForTest(value) {
|
|
201
|
-
|
|
78
|
+
keyringExpectedOverride = value;
|
|
202
79
|
}
|
|
203
|
-
/** Test-only: shrink retry attempts/delay so the persistent-failure path is
|
|
204
|
-
* fast. `null`/omitted restores defaults. */
|
|
205
80
|
export function _setKeyringRetryForTest(attempts, delayMs) {
|
|
206
|
-
|
|
207
|
-
|
|
81
|
+
keyringReadAttempts = attempts ?? 3;
|
|
82
|
+
keyringRetryDelayMs = delayMs ?? 300;
|
|
83
|
+
}
|
|
84
|
+
export function _setIdentityLockTimingForTest(waitTimeoutMs, pollIntervalMs) {
|
|
85
|
+
identityLockWaitTimeoutMs = waitTimeoutMs ?? 15_000;
|
|
86
|
+
identityLockPollIntervalMs = pollIntervalMs ?? 50;
|
|
208
87
|
}
|
|
209
|
-
function
|
|
210
|
-
return ms > 0 ? new Promise((
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
211
90
|
}
|
|
212
|
-
function
|
|
91
|
+
function serialize(kp) {
|
|
213
92
|
const payload = {
|
|
214
93
|
pk: Buffer.from(kp.publicKey).toString("base64"),
|
|
215
94
|
sk: Buffer.from(kp.secretKey).toString("base64"),
|
|
216
95
|
};
|
|
217
96
|
return JSON.stringify(payload);
|
|
218
97
|
}
|
|
219
|
-
function
|
|
98
|
+
function decodeCanonicalBase64(value, field) {
|
|
99
|
+
if (typeof value !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
100
|
+
throw new Error(`invalid ${field}`);
|
|
101
|
+
}
|
|
102
|
+
const decoded = Buffer.from(value, "base64");
|
|
103
|
+
if (decoded.toString("base64") !== value)
|
|
104
|
+
throw new Error(`invalid ${field}`);
|
|
105
|
+
return decoded;
|
|
106
|
+
}
|
|
107
|
+
function deserializeUnsafe(stored) {
|
|
220
108
|
const parsed = JSON.parse(stored);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
109
|
+
const publicKey = decodeCanonicalBase64(parsed.pk, "pk");
|
|
110
|
+
const secretKey = decodeCanonicalBase64(parsed.sk, "sk");
|
|
111
|
+
if (publicKey.length !== 32 || (secretKey.length !== 32 && secretKey.length !== 64))
|
|
112
|
+
throw new Error("invalid key length");
|
|
113
|
+
return { publicKey, secretKey };
|
|
114
|
+
}
|
|
115
|
+
function deserializeKeyringIdentity(stored, service) {
|
|
116
|
+
try {
|
|
117
|
+
return deserializeUnsafe(stored);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new KeyringIdentityError(service);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isNodeErrorWithCode(error, code) {
|
|
124
|
+
return typeof error === "object" && error !== null && "code" in error &&
|
|
125
|
+
error.code === code;
|
|
225
126
|
}
|
|
226
|
-
|
|
227
|
-
|
|
127
|
+
async function readKeypairFromFile() {
|
|
128
|
+
let raw;
|
|
228
129
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
130
|
+
raw = await readFile(IDENTITY_FILE, "utf8");
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (isNodeErrorWithCode(error, "ENOENT"))
|
|
134
|
+
return null;
|
|
135
|
+
throw new IdentityFileError(IDENTITY_FILE, "read_failed");
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return deserializeUnsafe(raw);
|
|
231
139
|
}
|
|
232
140
|
catch {
|
|
233
|
-
|
|
141
|
+
throw new IdentityFileError(IDENTITY_FILE, "invalid_format");
|
|
234
142
|
}
|
|
235
143
|
}
|
|
236
|
-
async function
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
144
|
+
async function syncIdentityDirectory() {
|
|
145
|
+
if (process.platform === "win32")
|
|
146
|
+
return;
|
|
147
|
+
const directory = await open(PI_DIR, "r");
|
|
240
148
|
try {
|
|
241
|
-
await
|
|
149
|
+
await directory.sync();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await directory.close();
|
|
242
153
|
}
|
|
243
|
-
|
|
244
|
-
|
|
154
|
+
}
|
|
155
|
+
async function writeKeypairToFile(kp) {
|
|
156
|
+
await ensurePrivateIdentityDirectory(PI_DIR);
|
|
157
|
+
const temporaryPath = join(PI_DIR, `.identity.json.${process.pid}.${randomUUID()}.tmp`);
|
|
158
|
+
let handle = null;
|
|
159
|
+
let published = false;
|
|
245
160
|
try {
|
|
161
|
+
handle = await open(temporaryPath, "wx", 0o600);
|
|
162
|
+
await handle.writeFile(serialize(kp), "utf8");
|
|
163
|
+
await handle.sync();
|
|
164
|
+
await handle.close();
|
|
165
|
+
handle = null;
|
|
166
|
+
await rename(temporaryPath, IDENTITY_FILE);
|
|
167
|
+
published = true;
|
|
246
168
|
await chmod(IDENTITY_FILE, 0o600);
|
|
169
|
+
await syncIdentityDirectory();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (handle)
|
|
173
|
+
await handle.close().catch(() => undefined);
|
|
174
|
+
if (!published) {
|
|
175
|
+
await unlink(temporaryPath).catch((error) => {
|
|
176
|
+
if (!isNodeErrorWithCode(error, "ENOENT"))
|
|
177
|
+
throw error;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
247
180
|
}
|
|
248
|
-
catch { /* not fatal */ }
|
|
249
181
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
* as a file and the browser PWA paired against THAT pubkey. If the
|
|
259
|
-
* platform keyring later becomes readable (D-Bus/libsecret installed, a
|
|
260
|
-
* desktop session, or a stale/other entry from another install), reading
|
|
261
|
-
* it first would mask the file identity — returning a DIFFERENT key, or
|
|
262
|
-
* (when the keyring is empty) minting a fresh one and persisting it —
|
|
263
|
-
* silently breaking the existing pairing. So when both exist, file wins.
|
|
264
|
-
* 2. New keyring service `dev.remotepi.pi` (read retried — a transiently
|
|
265
|
-
* locked Keychain throws; we don't treat that as "no key")
|
|
266
|
-
* 3. Old keyring service `dev.remotepi.mac` (migrate → step 2, delete old)
|
|
267
|
-
* 4. Generate a fresh keypair, BUT only when it's safe to: either both
|
|
268
|
-
* keyring reads succeeded and returned nothing (genuine first run), or
|
|
269
|
-
* the keyring is genuinely unavailable on a platform without a core one
|
|
270
|
-
* (headless Linux → a file identity is minted here). On macOS/Windows a
|
|
271
|
-
* persistent read failure with no file identity throws
|
|
272
|
-
* `KeyringUnavailableError` instead of minting a new key — generating
|
|
273
|
-
* there silently breaks existing pairing (the "lost pairing after idle"
|
|
274
|
-
* bug). `REMOTE_PI_ALLOW_FILE_IDENTITY=1` opts back into a file identity
|
|
275
|
-
* for headless macOS/Windows hosts.
|
|
276
|
-
*
|
|
277
|
-
* Idempotent: subsequent calls return the same identity. The migration
|
|
278
|
-
* runs at most once per machine (the old entry is deleted after copy).
|
|
279
|
-
*/
|
|
280
|
-
export async function getOrCreateEd25519Keypair() {
|
|
281
|
-
const backend = _getBackend();
|
|
282
|
-
// ── Path 0: an existing file-backed identity wins ──────────────────────
|
|
283
|
-
// `~/.pi/remote/identity.json` is only ever written by the headless/degraded
|
|
284
|
-
// fallback below (or an operator who set REMOTE_PI_ALLOW_FILE_IDENTITY=1) —
|
|
285
|
-
// never on a keyring-backed install. So its presence means THIS machine
|
|
286
|
-
// paired against the file key, and the keyring (readable or not, matching or
|
|
287
|
-
// not) must not be allowed to mask it. Short-circuit before touching the
|
|
288
|
-
// keyring so a keyring that later comes online can't return a different key,
|
|
289
|
-
// nor mint a fresh one over the file identity. No file → normal keyring
|
|
290
|
-
// resolution below; a headless first run still reaches Path B and mints one.
|
|
291
|
-
const existingFile = await _readKeypairFromFile();
|
|
292
|
-
if (existingFile)
|
|
293
|
-
return existingFile;
|
|
294
|
-
// ── Path A: keyring (retried) ──────────────────────────────────────────
|
|
295
|
-
// A throw here means the keyring op FAILED — but on macOS/Windows that is
|
|
296
|
-
// almost always a transiently locked Keychain (idle/just-woke machine), not
|
|
297
|
-
// a missing backend. `read` returns `undefined` for "no such entry" (the
|
|
298
|
-
// genuine first-run signal). So we retry on throw, and only a throw that
|
|
299
|
-
// SURVIVES every attempt drops us to Path B.
|
|
182
|
+
async function assertGenerationIsSafe(forceFile, cause) {
|
|
183
|
+
if (forceFile)
|
|
184
|
+
return;
|
|
185
|
+
const paired = await listPeers();
|
|
186
|
+
if (paired.length > 0)
|
|
187
|
+
throw new PairedIdentityMissingError(paired.length, cause);
|
|
188
|
+
}
|
|
189
|
+
async function resolveKeyringReads(store) {
|
|
300
190
|
let keyringError;
|
|
301
|
-
for (let attempt = 0; attempt <
|
|
191
|
+
for (let attempt = 0; attempt < keyringReadAttempts; attempt++) {
|
|
302
192
|
try {
|
|
303
|
-
const existing = await
|
|
193
|
+
const existing = await store.read(NEW_SERVICE, ACCOUNT);
|
|
304
194
|
if (existing)
|
|
305
|
-
return
|
|
306
|
-
const legacy = await
|
|
307
|
-
if (legacy)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
await backend.delete(OLD_SERVICE, ACCOUNT);
|
|
311
|
-
// Silent migration: writing the chat surface would be premature
|
|
312
|
-
// (Pi SDK isn't bound yet at this point in boot) and console
|
|
313
|
-
// output bleeds outside the TUI. The presence of an entry under
|
|
314
|
-
// NEW_SERVICE is itself the audit signal — re-running migration
|
|
315
|
-
// is idempotent and harmless.
|
|
316
|
-
return kp;
|
|
317
|
-
}
|
|
318
|
-
// Both reads SUCCEEDED and returned nothing → genuine first run on a
|
|
319
|
-
// working keyring. Generate and save to the new service.
|
|
320
|
-
const fresh = generateEd25519Keypair();
|
|
321
|
-
await backend.write(NEW_SERVICE, ACCOUNT, _serialize(fresh));
|
|
322
|
-
return fresh;
|
|
195
|
+
return { resolution: { kind: "new", stored: existing }, error: undefined };
|
|
196
|
+
const legacy = await store.read(OLD_SERVICE, ACCOUNT);
|
|
197
|
+
if (legacy)
|
|
198
|
+
return { resolution: { kind: "legacy", stored: legacy }, error: undefined };
|
|
199
|
+
return { resolution: { kind: "empty" }, error: undefined };
|
|
323
200
|
}
|
|
324
|
-
catch (
|
|
325
|
-
keyringError =
|
|
326
|
-
if (attempt <
|
|
327
|
-
|
|
328
|
-
await _sleep(_keyringRetryDelayMs * (attempt + 1));
|
|
201
|
+
catch (error) {
|
|
202
|
+
keyringError = error;
|
|
203
|
+
if (attempt < keyringReadAttempts - 1) {
|
|
204
|
+
await sleep(keyringRetryDelayMs * (attempt + 1));
|
|
329
205
|
}
|
|
330
206
|
}
|
|
331
207
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
208
|
+
return { resolution: null, error: keyringError };
|
|
209
|
+
}
|
|
210
|
+
async function getOrCreateUnderLock() {
|
|
211
|
+
const existingFile = await readKeypairFromFile();
|
|
212
|
+
if (existingFile)
|
|
213
|
+
return existingFile;
|
|
214
|
+
const store = getBackend();
|
|
215
|
+
const { resolution, error: keyringError } = await resolveKeyringReads(store);
|
|
216
|
+
const forceFile = process.env.REMOTE_PI_ALLOW_FILE_IDENTITY === "1";
|
|
217
|
+
if (resolution?.kind === "new") {
|
|
218
|
+
return deserializeKeyringIdentity(resolution.stored, NEW_SERVICE);
|
|
219
|
+
}
|
|
220
|
+
if (resolution?.kind === "legacy") {
|
|
221
|
+
const keypair = deserializeKeyringIdentity(resolution.stored, OLD_SERVICE);
|
|
222
|
+
await store.write(NEW_SERVICE, ACCOUNT, resolution.stored);
|
|
223
|
+
await store.delete(OLD_SERVICE, ACCOUNT);
|
|
224
|
+
return keypair;
|
|
225
|
+
}
|
|
226
|
+
if (resolution?.kind === "empty") {
|
|
227
|
+
await assertGenerationIsSafe(forceFile, undefined);
|
|
228
|
+
const fresh = generateEd25519Keypair();
|
|
229
|
+
await store.write(NEW_SERVICE, ACCOUNT, serialize(fresh));
|
|
230
|
+
return fresh;
|
|
231
|
+
}
|
|
232
|
+
const fromFile = await readKeypairFromFile();
|
|
337
233
|
if (fromFile)
|
|
338
234
|
return fromFile;
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
// - On a platform without a guaranteed keyring (headless Linux, no D-Bus),
|
|
342
|
-
// minting a file-backed identity is the documented, correct first-run
|
|
343
|
-
// behavior.
|
|
344
|
-
// - On macOS/Windows the keyring is a core OS service, so a persistent read
|
|
345
|
-
// failure means it's LOCKED/denied — NOT that we're a fresh install.
|
|
346
|
-
// Generating a new key here is exactly what silently broke pairing after
|
|
347
|
-
// a week idle, and the new key then masks the real Keychain identity via
|
|
348
|
-
// the file. So we FAIL LOUD instead, unless the operator explicitly
|
|
349
|
-
// opts into a file identity.
|
|
350
|
-
const forceFile = process.env.REMOTE_PI_ALLOW_FILE_IDENTITY === "1";
|
|
351
|
-
if (_keyringExpectedAvailable() && !forceFile) {
|
|
235
|
+
if (keyringExpectedAvailable() && !forceFile) {
|
|
352
236
|
throw new KeyringUnavailableError(keyringError);
|
|
353
237
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// different secret store than the interactive process which created the machine key; a fresh
|
|
357
|
-
// identity would no longer authenticate as the paired Host. Keep peers.json
|
|
358
|
-
// intact and fail loudly so the operator can restore access or pin the
|
|
359
|
-
// established identity to the file-backed store.
|
|
360
|
-
if (!forceFile) {
|
|
361
|
-
const paired = await listPeers();
|
|
362
|
-
if (paired.length > 0) {
|
|
363
|
-
throw new PairedIdentityMissingError(paired.length, keyringError);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
console.warn(_nativeBindingUnavailable()
|
|
367
|
-
// Issue #113: the @napi-rs/keyring native binding could not be loaded at
|
|
368
|
-
// all (typically a Bun-compiled pi). Name it explicitly — the error the
|
|
369
|
-
// loader prints blames npm's optional-dependency bug and sends people off
|
|
370
|
-
// to delete node_modules, which takes their other pi packages with it.
|
|
238
|
+
await assertGenerationIsSafe(forceFile, keyringError);
|
|
239
|
+
console.warn(nativeBindingUnavailable()
|
|
371
240
|
? "[remote-pi] @napi-rs/keyring native binding could not be loaded in this " +
|
|
372
|
-
`runtime; using file-backed identity at ${IDENTITY_FILE} (0600) instead. `
|
|
373
|
-
"This is expected on a Bun-built pi. Paired devices keyed to a previous " +
|
|
374
|
-
`keyring identity must be re-paired. ${String(keyringError)}`
|
|
241
|
+
`runtime; using file-backed identity at ${IDENTITY_FILE} (0600) instead. ${String(keyringError)}`
|
|
375
242
|
: "[remote-pi] keyring unavailable; using file-backed identity at " +
|
|
376
243
|
`${IDENTITY_FILE}. ${String(keyringError)}`);
|
|
377
244
|
const fresh = generateEd25519Keypair();
|
|
378
|
-
await
|
|
245
|
+
await writeKeypairToFile(fresh);
|
|
379
246
|
return fresh;
|
|
380
247
|
}
|
|
381
|
-
|
|
382
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Returns the stable Host identity. Existing file identity wins; otherwise all
|
|
250
|
+
* keyring reads, migration, generation, and persistence run under the fixed
|
|
251
|
+
* cross-process identity lock. Every source is read again after lock acquisition.
|
|
252
|
+
*/
|
|
253
|
+
export async function getOrCreateEd25519Keypair() {
|
|
254
|
+
const existingFile = await readKeypairFromFile();
|
|
255
|
+
if (existingFile)
|
|
256
|
+
return existingFile;
|
|
257
|
+
return withIdentityLock(PI_DIR, getOrCreateUnderLock, {
|
|
258
|
+
waitTimeoutMs: identityLockWaitTimeoutMs,
|
|
259
|
+
pollIntervalMs: identityLockPollIntervalMs,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
383
262
|
export const _IDENTITY_FILE_FOR_TEST = IDENTITY_FILE;
|
|
384
|
-
|
|
263
|
+
export const _IDENTITY_LOCK_FILE_FOR_TEST = IDENTITY_LOCK_FILE;
|
|
385
264
|
export const _unlinkIdentityFileForTest = async () => {
|
|
386
265
|
try {
|
|
387
266
|
await unlink(IDENTITY_FILE);
|
|
388
267
|
}
|
|
389
|
-
catch {
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (!isNodeErrorWithCode(error, "ENOENT"))
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
390
272
|
};
|
|
391
273
|
//# sourceMappingURL=storage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/pairing/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/pairing/storage.ts"],"names":[],"mappings":"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"}
|