@parity/product-sdk-terminal 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 +201 -0
- package/README.md +261 -0
- package/dist/chunk-5MCLSFKQ.js +210 -0
- package/dist/chunk-5MCLSFKQ.js.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +327 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.mjs +76 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +9 -0
- package/dist/register.js.map +1 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.js +294 -0
- package/dist/testing.js.map +1 -0
- package/package.json +64 -0
- package/scripts/patch-wasm.sh +27 -0
package/dist/register.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/register.ts
|
|
2
|
+
import { register } from "module";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
var loaderPath = join(__dirname, "loader.mjs");
|
|
8
|
+
register(pathToFileURL(loaderPath).href, { parentURL: import.meta.url });
|
|
9
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/register.ts"],"sourcesContent":["/**\n * Register the verifiablejs Node.js loader hook.\n *\n * Must be loaded BEFORE the application entry point so the loader\n * intercepts `verifiablejs/bundler` imports from the host-papp SDK.\n *\n * @example\n * ```bash\n * node --import @parity/product-sdk-terminal/register app.js\n * tsx --import @parity/product-sdk-terminal/register app.ts\n * ```\n */\nimport { register } from \"node:module\";\nimport { pathToFileURL } from \"node:url\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst loaderPath = join(__dirname, \"loader.mjs\");\n\nregister(pathToFileURL(loaderPath).href, { parentURL: import.meta.url });\n"],"mappings":";AAYA,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAE9B,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AACxD,IAAM,aAAa,KAAK,WAAW,YAAY;AAE/C,SAAS,cAAc,UAAU,EAAE,MAAM,EAAE,WAAW,YAAY,IAAI,CAAC;","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
interface CreateTestSessionOptions {
|
|
2
|
+
/** Unique app identifier. Must match the one passed to `createTerminalAdapter`. */
|
|
3
|
+
appId: string;
|
|
4
|
+
/**
|
|
5
|
+
* Directory where session files are written. Must match the one the
|
|
6
|
+
* CLI reads from — pass the same value to `createTerminalAdapter`'s
|
|
7
|
+
* `storageDir` option.
|
|
8
|
+
*/
|
|
9
|
+
storageDir: string;
|
|
10
|
+
/** BIP39 mnemonic for the local (host-side) account. Default: freshly generated. */
|
|
11
|
+
localMnemonic?: string;
|
|
12
|
+
/** Derivation path for the local account. Default: `"//wallet//sso"` (matches production). */
|
|
13
|
+
localDerivation?: string;
|
|
14
|
+
/** BIP39 mnemonic for the remote (phone) account. Default: freshly generated. */
|
|
15
|
+
remoteMnemonic?: string;
|
|
16
|
+
/** Derivation path for the remote account. Default: `""`. */
|
|
17
|
+
remoteDerivation?: string;
|
|
18
|
+
/** Stable session id. Default: random nanoid(12). */
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to write the encrypted `UserSecrets_<id>` file. Default: `true`.
|
|
22
|
+
* Set to `false` to exercise recovery paths where a session exists on disk
|
|
23
|
+
* but its secrets are missing.
|
|
24
|
+
*/
|
|
25
|
+
includeSecrets?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface TestSession {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
/** Sr25519 public key of the local (host) account, 32 bytes. */
|
|
30
|
+
localAccountId: Uint8Array;
|
|
31
|
+
/** Sr25519 public key of the remote (phone) account, 32 bytes. */
|
|
32
|
+
remoteAccountId: Uint8Array;
|
|
33
|
+
localMnemonic: string;
|
|
34
|
+
localDerivation: string;
|
|
35
|
+
remoteMnemonic: string;
|
|
36
|
+
remoteDerivation: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Write a valid persisted session to `storageDir`, as if the user had
|
|
40
|
+
* completed QR pairing + attestation. A `TerminalAdapter` created with the
|
|
41
|
+
* same `appId` + `storageDir` will emit the synthesized session from
|
|
42
|
+
* `adapter.sessions.sessions`.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* **Dev utility — not a stable contract.** This helper hand-rolls SCALE codecs
|
|
46
|
+
* that mirror an internal on-disk format used by `@novasamatech/host-papp`'s
|
|
47
|
+
* session repositories. The format is not part of host-papp's documented public
|
|
48
|
+
* API and may change on minor version bumps. The `testing.interop.test.ts`
|
|
49
|
+
* round-trip test fails loudly when the format drifts. Use this only in test code.
|
|
50
|
+
*
|
|
51
|
+
* ## Limits
|
|
52
|
+
*
|
|
53
|
+
* - **Signing does not round-trip.** Sending a request via `session.signRaw`
|
|
54
|
+
* still goes out over the statement store and expects a real phone to
|
|
55
|
+
* respond. Use this helper for flows that test session discovery,
|
|
56
|
+
* persistence, and logout — not end-to-end signing.
|
|
57
|
+
* - **Signing attempts surface `NoAllowanceError`.** The synthesized local
|
|
58
|
+
* account was never registered on the People chain, so the first write
|
|
59
|
+
* to the statement store fails with `NoAllowanceError`. That's the same
|
|
60
|
+
* error path the CLI sees when a previously valid session's on-chain
|
|
61
|
+
* attestation has expired, so tests that assert "CLI handles an expired
|
|
62
|
+
* session" *can* be written against a synthesized session even though
|
|
63
|
+
* there's no `expiresAt` knob to turn.
|
|
64
|
+
* - **No `expiresAt`.** The on-disk codec has no expiry field; validity is
|
|
65
|
+
* tracked via on-chain attestation state. See above for how expiry-path
|
|
66
|
+
* tests still work in practice.
|
|
67
|
+
* - **Corrupted-session cases** don't need a helper — write garbage to
|
|
68
|
+
* `<storageDir>/<appId>_SsoSessions.json` with `fs.writeFile` directly.
|
|
69
|
+
* - **Repeated calls replace the session list.** Each call writes a fresh
|
|
70
|
+
* single-entry `SsoSessions` file, so calling twice on the same
|
|
71
|
+
* `storageDir`+`appId` leaves only the second session on disk. Use a
|
|
72
|
+
* fresh `mkdtempSync` per test to keep cases isolated.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { mkdtempSync } from "node:fs";
|
|
77
|
+
* import { tmpdir } from "node:os";
|
|
78
|
+
* import { join } from "node:path";
|
|
79
|
+
* import { createTestSession } from "@parity/product-sdk-terminal/testing";
|
|
80
|
+
* import { createTerminalAdapter, waitForSessions } from "@parity/product-sdk-terminal";
|
|
81
|
+
*
|
|
82
|
+
* const storageDir = mkdtempSync(join(tmpdir(), "e2e-"));
|
|
83
|
+
* const { sessionId } = await createTestSession({ appId: "dot-cli", storageDir });
|
|
84
|
+
*
|
|
85
|
+
* const adapter = createTerminalAdapter({ appId: "dot-cli", metadataUrl: "…", storageDir });
|
|
86
|
+
* const sessions = await waitForSessions(adapter);
|
|
87
|
+
* // sessions[0].id === sessionId
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare function createTestSession(options: CreateTestSessionOptions): Promise<TestSession>;
|
|
91
|
+
|
|
92
|
+
export { type CreateTestSessionOptions, type TestSession, createTestSession };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sanitizeKey
|
|
3
|
+
} from "./chunk-5MCLSFKQ.js";
|
|
4
|
+
|
|
5
|
+
// src/testing.ts
|
|
6
|
+
import { gcm } from "@noble/ciphers/aes.js";
|
|
7
|
+
import { p256 } from "@noble/curves/nist.js";
|
|
8
|
+
import { blake2b } from "@noble/hashes/blake2.js";
|
|
9
|
+
import {
|
|
10
|
+
LocalSessionAccountCodec,
|
|
11
|
+
RemoteSessionAccountCodec,
|
|
12
|
+
createAccountId,
|
|
13
|
+
createLocalSessionAccount,
|
|
14
|
+
createRemoteSessionAccount,
|
|
15
|
+
createSr25519Secret,
|
|
16
|
+
deriveSr25519PublicKey
|
|
17
|
+
} from "@novasamatech/statement-store";
|
|
18
|
+
import { toHex } from "@polkadot-api/utils";
|
|
19
|
+
import {
|
|
20
|
+
entropyToMiniSecret,
|
|
21
|
+
generateMnemonic,
|
|
22
|
+
mnemonicToEntropy
|
|
23
|
+
} from "@polkadot-labs/hdkd-helpers";
|
|
24
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
25
|
+
import { join } from "path";
|
|
26
|
+
import { nanoid } from "nanoid";
|
|
27
|
+
import { Bytes, Struct, Vector, str } from "scale-ts";
|
|
28
|
+
var storedUserSessionCodec = Struct({
|
|
29
|
+
id: str,
|
|
30
|
+
localAccount: LocalSessionAccountCodec,
|
|
31
|
+
remoteAccount: RemoteSessionAccountCodec
|
|
32
|
+
});
|
|
33
|
+
var sessionsCodec = Vector(storedUserSessionCodec);
|
|
34
|
+
var storedUserSecretsCodec = Struct({
|
|
35
|
+
ssSecret: Bytes(),
|
|
36
|
+
encrSecret: Bytes(),
|
|
37
|
+
entropy: Bytes()
|
|
38
|
+
});
|
|
39
|
+
function encryptSecrets(appId, plaintext) {
|
|
40
|
+
const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });
|
|
41
|
+
const nonce = blake2b(new TextEncoder().encode("nonce"), { dkLen: 32 });
|
|
42
|
+
return toHex(gcm(key, nonce).encrypt(plaintext));
|
|
43
|
+
}
|
|
44
|
+
function p256SecretFromEntropy(entropy) {
|
|
45
|
+
const seed = new Uint8Array(48);
|
|
46
|
+
seed.set(entropyToMiniSecret(entropy));
|
|
47
|
+
return p256.keygen(seed).secretKey;
|
|
48
|
+
}
|
|
49
|
+
async function createTestSession(options) {
|
|
50
|
+
await mkdir(options.storageDir, { recursive: true });
|
|
51
|
+
const localMnemonic = options.localMnemonic ?? generateMnemonic();
|
|
52
|
+
const localDerivation = options.localDerivation ?? "//wallet//sso";
|
|
53
|
+
const localEntropy = mnemonicToEntropy(localMnemonic);
|
|
54
|
+
const localSecret = createSr25519Secret(localEntropy, localDerivation);
|
|
55
|
+
const localPublicKey = deriveSr25519PublicKey(localSecret);
|
|
56
|
+
const localEncrSecret = p256SecretFromEntropy(localEntropy);
|
|
57
|
+
const remoteMnemonic = options.remoteMnemonic ?? generateMnemonic();
|
|
58
|
+
const remoteDerivation = options.remoteDerivation ?? "";
|
|
59
|
+
const remoteEntropy = mnemonicToEntropy(remoteMnemonic);
|
|
60
|
+
const remoteSecret = createSr25519Secret(remoteEntropy, remoteDerivation);
|
|
61
|
+
const remotePublicKey = deriveSr25519PublicKey(remoteSecret);
|
|
62
|
+
const remoteEncrPublicKey = p256.getPublicKey(p256SecretFromEntropy(remoteEntropy), false);
|
|
63
|
+
const sharedSecret = p256.getSharedSecret(localEncrSecret, remoteEncrPublicKey).slice(1, 33);
|
|
64
|
+
const sessionId = options.sessionId ?? nanoid(12);
|
|
65
|
+
const session = {
|
|
66
|
+
id: sessionId,
|
|
67
|
+
localAccount: createLocalSessionAccount(createAccountId(localPublicKey), void 0),
|
|
68
|
+
remoteAccount: createRemoteSessionAccount(
|
|
69
|
+
createAccountId(remotePublicKey),
|
|
70
|
+
sharedSecret,
|
|
71
|
+
void 0
|
|
72
|
+
)
|
|
73
|
+
};
|
|
74
|
+
await writeFile(
|
|
75
|
+
join(options.storageDir, `${sanitizeKey(options.appId, "SsoSessions")}.json`),
|
|
76
|
+
toHex(sessionsCodec.enc([session])),
|
|
77
|
+
"utf-8"
|
|
78
|
+
);
|
|
79
|
+
const includeSecrets = options.includeSecrets ?? true;
|
|
80
|
+
if (includeSecrets) {
|
|
81
|
+
const encoded = storedUserSecretsCodec.enc({
|
|
82
|
+
ssSecret: localSecret,
|
|
83
|
+
encrSecret: localEncrSecret,
|
|
84
|
+
entropy: localEntropy
|
|
85
|
+
});
|
|
86
|
+
await writeFile(
|
|
87
|
+
join(
|
|
88
|
+
options.storageDir,
|
|
89
|
+
`${sanitizeKey(options.appId, `UserSecrets_${sessionId}`)}.json`
|
|
90
|
+
),
|
|
91
|
+
encryptSecrets(options.appId, encoded),
|
|
92
|
+
"utf-8"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
sessionId,
|
|
97
|
+
localAccountId: localPublicKey,
|
|
98
|
+
remoteAccountId: remotePublicKey,
|
|
99
|
+
localMnemonic,
|
|
100
|
+
localDerivation,
|
|
101
|
+
remoteMnemonic,
|
|
102
|
+
remoteDerivation
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (void 0) {
|
|
106
|
+
const { describe, test, expect, beforeEach, afterAll } = void 0;
|
|
107
|
+
const { mkdtemp, readFile, rm } = await null;
|
|
108
|
+
const { tmpdir } = await null;
|
|
109
|
+
const { fromHex } = await null;
|
|
110
|
+
const LOCAL_MNEMONIC = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
|
111
|
+
const REMOTE_MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
112
|
+
let storageDir;
|
|
113
|
+
beforeEach(async () => {
|
|
114
|
+
storageDir = await mkdtemp(join(tmpdir(), "terminal-testing-"));
|
|
115
|
+
});
|
|
116
|
+
afterAll(async () => {
|
|
117
|
+
try {
|
|
118
|
+
await rm(storageDir, { recursive: true });
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
describe("createTestSession", () => {
|
|
123
|
+
test("writes both SsoSessions and UserSecrets files by default", async () => {
|
|
124
|
+
const result = await createTestSession({
|
|
125
|
+
appId: "my-app",
|
|
126
|
+
storageDir,
|
|
127
|
+
localMnemonic: LOCAL_MNEMONIC,
|
|
128
|
+
remoteMnemonic: REMOTE_MNEMONIC
|
|
129
|
+
});
|
|
130
|
+
const sessions = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
|
|
131
|
+
expect(sessions).toMatch(/^0x[0-9a-f]+$/);
|
|
132
|
+
const secrets = await readFile(
|
|
133
|
+
join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`),
|
|
134
|
+
"utf-8"
|
|
135
|
+
);
|
|
136
|
+
expect(secrets).toMatch(/^0x[0-9a-f]+$/);
|
|
137
|
+
});
|
|
138
|
+
test("omits UserSecrets file when includeSecrets is false", async () => {
|
|
139
|
+
const result = await createTestSession({
|
|
140
|
+
appId: "my-app",
|
|
141
|
+
storageDir,
|
|
142
|
+
localMnemonic: LOCAL_MNEMONIC,
|
|
143
|
+
remoteMnemonic: REMOTE_MNEMONIC,
|
|
144
|
+
includeSecrets: false
|
|
145
|
+
});
|
|
146
|
+
await expect(
|
|
147
|
+
readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8")
|
|
148
|
+
).resolves.toMatch(/^0x/);
|
|
149
|
+
await expect(
|
|
150
|
+
readFile(join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`), "utf-8")
|
|
151
|
+
).rejects.toThrow(/ENOENT/);
|
|
152
|
+
});
|
|
153
|
+
test("SsoSessions file decodes with the host-papp session codec shape", async () => {
|
|
154
|
+
const result = await createTestSession({
|
|
155
|
+
appId: "my-app",
|
|
156
|
+
storageDir,
|
|
157
|
+
localMnemonic: LOCAL_MNEMONIC,
|
|
158
|
+
remoteMnemonic: REMOTE_MNEMONIC,
|
|
159
|
+
sessionId: "stable-test-id"
|
|
160
|
+
});
|
|
161
|
+
const hex = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
|
|
162
|
+
const decoded = sessionsCodec.dec(fromHex(hex));
|
|
163
|
+
expect(decoded).toHaveLength(1);
|
|
164
|
+
expect(decoded[0].id).toBe("stable-test-id");
|
|
165
|
+
expect(decoded[0].localAccount.accountId).toEqual(result.localAccountId);
|
|
166
|
+
expect(decoded[0].localAccount.pin).toBeUndefined();
|
|
167
|
+
expect(decoded[0].remoteAccount.accountId).toEqual(result.remoteAccountId);
|
|
168
|
+
expect(decoded[0].remoteAccount.publicKey).toHaveLength(32);
|
|
169
|
+
expect(decoded[0].remoteAccount.pin).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
test("localAccountId matches the Sr25519 public key derived from the mnemonic", async () => {
|
|
172
|
+
const result = await createTestSession({
|
|
173
|
+
appId: "my-app",
|
|
174
|
+
storageDir,
|
|
175
|
+
localMnemonic: LOCAL_MNEMONIC,
|
|
176
|
+
remoteMnemonic: REMOTE_MNEMONIC
|
|
177
|
+
});
|
|
178
|
+
const expected = deriveSr25519PublicKey(
|
|
179
|
+
createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), "//wallet//sso")
|
|
180
|
+
);
|
|
181
|
+
expect(result.localAccountId).toEqual(expected);
|
|
182
|
+
expect(result.localAccountId).toHaveLength(32);
|
|
183
|
+
});
|
|
184
|
+
test("UserSecrets file decrypts and decodes with the host-papp secret codec shape", async () => {
|
|
185
|
+
const appId = "my-app";
|
|
186
|
+
const result = await createTestSession({
|
|
187
|
+
appId,
|
|
188
|
+
storageDir,
|
|
189
|
+
localMnemonic: LOCAL_MNEMONIC,
|
|
190
|
+
remoteMnemonic: REMOTE_MNEMONIC
|
|
191
|
+
});
|
|
192
|
+
const hex = await readFile(
|
|
193
|
+
join(storageDir, `${appId}_UserSecrets_${result.sessionId}.json`),
|
|
194
|
+
"utf-8"
|
|
195
|
+
);
|
|
196
|
+
const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });
|
|
197
|
+
const nonce = blake2b(new TextEncoder().encode("nonce"), { dkLen: 32 });
|
|
198
|
+
const decrypted = gcm(key, nonce).decrypt(fromHex(hex));
|
|
199
|
+
const decoded = storedUserSecretsCodec.dec(decrypted);
|
|
200
|
+
expect(decoded.entropy).toEqual(mnemonicToEntropy(LOCAL_MNEMONIC));
|
|
201
|
+
expect(decoded.ssSecret).toHaveLength(64);
|
|
202
|
+
expect(decoded.encrSecret).toHaveLength(32);
|
|
203
|
+
});
|
|
204
|
+
test("different appIds produce files under different prefixes", async () => {
|
|
205
|
+
await createTestSession({ appId: "app-a", storageDir, sessionId: "id" });
|
|
206
|
+
await createTestSession({ appId: "app-b", storageDir, sessionId: "id" });
|
|
207
|
+
await expect(
|
|
208
|
+
readFile(join(storageDir, "app-a_SsoSessions.json"), "utf-8")
|
|
209
|
+
).resolves.toMatch(/^0x/);
|
|
210
|
+
await expect(
|
|
211
|
+
readFile(join(storageDir, "app-b_SsoSessions.json"), "utf-8")
|
|
212
|
+
).resolves.toMatch(/^0x/);
|
|
213
|
+
});
|
|
214
|
+
test("sanitizes appIds with special characters", async () => {
|
|
215
|
+
await createTestSession({
|
|
216
|
+
appId: "app/with spaces",
|
|
217
|
+
storageDir,
|
|
218
|
+
sessionId: "id"
|
|
219
|
+
});
|
|
220
|
+
await expect(
|
|
221
|
+
readFile(join(storageDir, "app_with_spaces_SsoSessions.json"), "utf-8")
|
|
222
|
+
).resolves.toMatch(/^0x/);
|
|
223
|
+
});
|
|
224
|
+
test("creates storageDir when it does not yet exist", async () => {
|
|
225
|
+
const nested = join(storageDir, "does", "not", "exist");
|
|
226
|
+
await createTestSession({ appId: "my-app", storageDir: nested });
|
|
227
|
+
await expect(
|
|
228
|
+
readFile(join(nested, "my-app_SsoSessions.json"), "utf-8")
|
|
229
|
+
).resolves.toMatch(/^0x/);
|
|
230
|
+
});
|
|
231
|
+
test("generates fresh mnemonics when none are supplied", async () => {
|
|
232
|
+
const a = await createTestSession({ appId: "a", storageDir });
|
|
233
|
+
const b = await createTestSession({ appId: "b", storageDir });
|
|
234
|
+
expect(a.localMnemonic).not.toBe(b.localMnemonic);
|
|
235
|
+
expect(a.remoteMnemonic).not.toBe(b.remoteMnemonic);
|
|
236
|
+
expect(a.sessionId).not.toBe(b.sessionId);
|
|
237
|
+
});
|
|
238
|
+
test("respects an explicit sessionId", async () => {
|
|
239
|
+
const result = await createTestSession({
|
|
240
|
+
appId: "my-app",
|
|
241
|
+
storageDir,
|
|
242
|
+
sessionId: "pinned-id"
|
|
243
|
+
});
|
|
244
|
+
expect(result.sessionId).toBe("pinned-id");
|
|
245
|
+
await expect(
|
|
246
|
+
readFile(join(storageDir, "my-app_UserSecrets_pinned-id.json"), "utf-8")
|
|
247
|
+
).resolves.toMatch(/^0x/);
|
|
248
|
+
});
|
|
249
|
+
test("respects a custom localDerivation", async () => {
|
|
250
|
+
const result = await createTestSession({
|
|
251
|
+
appId: "my-app",
|
|
252
|
+
storageDir,
|
|
253
|
+
localMnemonic: LOCAL_MNEMONIC,
|
|
254
|
+
remoteMnemonic: REMOTE_MNEMONIC,
|
|
255
|
+
localDerivation: "//custom//path"
|
|
256
|
+
});
|
|
257
|
+
expect(result.localDerivation).toBe("//custom//path");
|
|
258
|
+
const expected = deriveSr25519PublicKey(
|
|
259
|
+
createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), "//custom//path")
|
|
260
|
+
);
|
|
261
|
+
expect(result.localAccountId).toEqual(expected);
|
|
262
|
+
});
|
|
263
|
+
test("repeated calls replace the session list", async () => {
|
|
264
|
+
const first = await createTestSession({
|
|
265
|
+
appId: "my-app",
|
|
266
|
+
storageDir,
|
|
267
|
+
sessionId: "first"
|
|
268
|
+
});
|
|
269
|
+
const second = await createTestSession({
|
|
270
|
+
appId: "my-app",
|
|
271
|
+
storageDir,
|
|
272
|
+
sessionId: "second"
|
|
273
|
+
});
|
|
274
|
+
const hex = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
|
|
275
|
+
const decoded = sessionsCodec.dec(fromHex(hex));
|
|
276
|
+
expect(decoded).toHaveLength(1);
|
|
277
|
+
expect(decoded[0].id).toBe("second");
|
|
278
|
+
await expect(
|
|
279
|
+
readFile(join(storageDir, `my-app_UserSecrets_${first.sessionId}.json`), "utf-8")
|
|
280
|
+
).resolves.toMatch(/^0x/);
|
|
281
|
+
await expect(
|
|
282
|
+
readFile(join(storageDir, `my-app_UserSecrets_${second.sessionId}.json`), "utf-8")
|
|
283
|
+
).resolves.toMatch(/^0x/);
|
|
284
|
+
});
|
|
285
|
+
test("AccountIdCodec is Bytes(32) \u2014 keep this invariant if upstream changes", () => {
|
|
286
|
+
const encoded = AccountIdCodec.enc(createAccountId(new Uint8Array(32).fill(7)));
|
|
287
|
+
expect(encoded).toHaveLength(32);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
export {
|
|
292
|
+
createTestSession
|
|
293
|
+
};
|
|
294
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Test helpers for synthesizing persisted sessions.\n *\n * Production sessions are persisted to the storage directory by\n * `createNodeStorageAdapter` in the SCALE-encoded / AES-GCM-encrypted\n * format defined by `@novasamatech/host-papp`'s internal repositories.\n * There is no public API on the adapter to write a session without going\n * through the QR pairing + on-chain attestation flow, which makes E2E\n * testing of session-dependent CLI flows impossible without a real phone.\n *\n * This module mirrors that format using only public primitives, so E2E\n * tests can inject a known-good session file. The `testing.test.ts`\n * integration test reads the synthesized files back through the real\n * `ssoSessionRepository` + `userSecretRepository` from `@novasamatech/host-papp`\n * and fails if the upstream format ever drifts from our reproduction.\n */\nimport { gcm } from \"@noble/ciphers/aes.js\";\nimport { p256 } from \"@noble/curves/nist.js\";\nimport { blake2b } from \"@noble/hashes/blake2.js\";\nimport {\n AccountIdCodec,\n LocalSessionAccountCodec,\n RemoteSessionAccountCodec,\n createAccountId,\n createLocalSessionAccount,\n createRemoteSessionAccount,\n createSr25519Secret,\n deriveSr25519PublicKey,\n} from \"@novasamatech/statement-store\";\nimport { toHex } from \"@polkadot-api/utils\";\nimport {\n entropyToMiniSecret,\n generateMnemonic,\n mnemonicToEntropy,\n} from \"@polkadot-labs/hdkd-helpers\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { nanoid } from \"nanoid\";\nimport { Bytes, Struct, Vector, str } from \"scale-ts\";\n\nimport { sanitizeKey } from \"./node-storage.js\";\n\n// Mirrors the internal codec in @novasamatech/host-papp's userSessionRepository.\nconst storedUserSessionCodec = Struct({\n id: str,\n localAccount: LocalSessionAccountCodec,\n remoteAccount: RemoteSessionAccountCodec,\n});\nconst sessionsCodec = Vector(storedUserSessionCodec);\n\n// Mirrors the internal StoredUserSecretsCodec in host-papp's userSecretRepository.\nconst storedUserSecretsCodec = Struct({\n ssSecret: Bytes(),\n encrSecret: Bytes(),\n entropy: Bytes(),\n});\n\n// Mirrors host-papp's userSecretRepository AES-GCM wrapper: the encryption\n// key is blake2b(appId, 16) and the nonce is blake2b(\"nonce\", 32). The appId\n// doubles as the salt, so sessions written by this helper are only readable\n// by a TerminalAdapter configured with the same appId.\nfunction encryptSecrets(appId: string, plaintext: Uint8Array): string {\n const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });\n const nonce = blake2b(new TextEncoder().encode(\"nonce\"), { dkLen: 32 });\n return toHex(gcm(key, nonce).encrypt(plaintext));\n}\n\n/** Mirrors host-papp's createEncrSecret: pad the mini-secret to 48 bytes and feed it to P256 keygen. */\nfunction p256SecretFromEntropy(entropy: Uint8Array): Uint8Array {\n const seed = new Uint8Array(48);\n seed.set(entropyToMiniSecret(entropy));\n return p256.keygen(seed).secretKey;\n}\n\nexport interface CreateTestSessionOptions {\n /** Unique app identifier. Must match the one passed to `createTerminalAdapter`. */\n appId: string;\n /**\n * Directory where session files are written. Must match the one the\n * CLI reads from — pass the same value to `createTerminalAdapter`'s\n * `storageDir` option.\n */\n storageDir: string;\n /** BIP39 mnemonic for the local (host-side) account. Default: freshly generated. */\n localMnemonic?: string;\n /** Derivation path for the local account. Default: `\"//wallet//sso\"` (matches production). */\n localDerivation?: string;\n /** BIP39 mnemonic for the remote (phone) account. Default: freshly generated. */\n remoteMnemonic?: string;\n /** Derivation path for the remote account. Default: `\"\"`. */\n remoteDerivation?: string;\n /** Stable session id. Default: random nanoid(12). */\n sessionId?: string;\n /**\n * Whether to write the encrypted `UserSecrets_<id>` file. Default: `true`.\n * Set to `false` to exercise recovery paths where a session exists on disk\n * but its secrets are missing.\n */\n includeSecrets?: boolean;\n}\n\nexport interface TestSession {\n sessionId: string;\n /** Sr25519 public key of the local (host) account, 32 bytes. */\n localAccountId: Uint8Array;\n /** Sr25519 public key of the remote (phone) account, 32 bytes. */\n remoteAccountId: Uint8Array;\n localMnemonic: string;\n localDerivation: string;\n remoteMnemonic: string;\n remoteDerivation: string;\n}\n\n/**\n * Write a valid persisted session to `storageDir`, as if the user had\n * completed QR pairing + attestation. A `TerminalAdapter` created with the\n * same `appId` + `storageDir` will emit the synthesized session from\n * `adapter.sessions.sessions`.\n *\n * @remarks\n * **Dev utility — not a stable contract.** This helper hand-rolls SCALE codecs\n * that mirror an internal on-disk format used by `@novasamatech/host-papp`'s\n * session repositories. The format is not part of host-papp's documented public\n * API and may change on minor version bumps. The `testing.interop.test.ts`\n * round-trip test fails loudly when the format drifts. Use this only in test code.\n *\n * ## Limits\n *\n * - **Signing does not round-trip.** Sending a request via `session.signRaw`\n * still goes out over the statement store and expects a real phone to\n * respond. Use this helper for flows that test session discovery,\n * persistence, and logout — not end-to-end signing.\n * - **Signing attempts surface `NoAllowanceError`.** The synthesized local\n * account was never registered on the People chain, so the first write\n * to the statement store fails with `NoAllowanceError`. That's the same\n * error path the CLI sees when a previously valid session's on-chain\n * attestation has expired, so tests that assert \"CLI handles an expired\n * session\" *can* be written against a synthesized session even though\n * there's no `expiresAt` knob to turn.\n * - **No `expiresAt`.** The on-disk codec has no expiry field; validity is\n * tracked via on-chain attestation state. See above for how expiry-path\n * tests still work in practice.\n * - **Corrupted-session cases** don't need a helper — write garbage to\n * `<storageDir>/<appId>_SsoSessions.json` with `fs.writeFile` directly.\n * - **Repeated calls replace the session list.** Each call writes a fresh\n * single-entry `SsoSessions` file, so calling twice on the same\n * `storageDir`+`appId` leaves only the second session on disk. Use a\n * fresh `mkdtempSync` per test to keep cases isolated.\n *\n * @example\n * ```ts\n * import { mkdtempSync } from \"node:fs\";\n * import { tmpdir } from \"node:os\";\n * import { join } from \"node:path\";\n * import { createTestSession } from \"@parity/product-sdk-terminal/testing\";\n * import { createTerminalAdapter, waitForSessions } from \"@parity/product-sdk-terminal\";\n *\n * const storageDir = mkdtempSync(join(tmpdir(), \"e2e-\"));\n * const { sessionId } = await createTestSession({ appId: \"dot-cli\", storageDir });\n *\n * const adapter = createTerminalAdapter({ appId: \"dot-cli\", metadataUrl: \"…\", storageDir });\n * const sessions = await waitForSessions(adapter);\n * // sessions[0].id === sessionId\n * ```\n */\nexport async function createTestSession(options: CreateTestSessionOptions): Promise<TestSession> {\n await mkdir(options.storageDir, { recursive: true });\n\n const localMnemonic = options.localMnemonic ?? generateMnemonic();\n const localDerivation = options.localDerivation ?? \"//wallet//sso\";\n const localEntropy = mnemonicToEntropy(localMnemonic);\n const localSecret = createSr25519Secret(localEntropy, localDerivation);\n const localPublicKey = deriveSr25519PublicKey(localSecret);\n const localEncrSecret = p256SecretFromEntropy(localEntropy);\n\n const remoteMnemonic = options.remoteMnemonic ?? generateMnemonic();\n const remoteDerivation = options.remoteDerivation ?? \"\";\n const remoteEntropy = mnemonicToEntropy(remoteMnemonic);\n const remoteSecret = createSr25519Secret(remoteEntropy, remoteDerivation);\n const remotePublicKey = deriveSr25519PublicKey(remoteSecret);\n const remoteEncrPublicKey = p256.getPublicKey(p256SecretFromEntropy(remoteEntropy), false);\n\n // In production, `remoteAccount.publicKey` is the ECDH shared secret\n // between the host's P256 encryption key and the phone's P256 encryption\n // key. We compute the same thing from two mnemonic-derived P256 keys so\n // the synthesized session is cryptographically well-formed.\n const sharedSecret = p256.getSharedSecret(localEncrSecret, remoteEncrPublicKey).slice(1, 33);\n\n const sessionId = options.sessionId ?? nanoid(12);\n\n const session = {\n id: sessionId,\n localAccount: createLocalSessionAccount(createAccountId(localPublicKey), undefined),\n remoteAccount: createRemoteSessionAccount(\n createAccountId(remotePublicKey),\n sharedSecret,\n undefined,\n ),\n };\n\n await writeFile(\n join(options.storageDir, `${sanitizeKey(options.appId, \"SsoSessions\")}.json`),\n toHex(sessionsCodec.enc([session])),\n \"utf-8\",\n );\n\n const includeSecrets = options.includeSecrets ?? true;\n if (includeSecrets) {\n const encoded = storedUserSecretsCodec.enc({\n ssSecret: localSecret,\n encrSecret: localEncrSecret,\n entropy: localEntropy,\n });\n await writeFile(\n join(\n options.storageDir,\n `${sanitizeKey(options.appId, `UserSecrets_${sessionId}`)}.json`,\n ),\n encryptSecrets(options.appId, encoded),\n \"utf-8\",\n );\n }\n\n return {\n sessionId,\n localAccountId: localPublicKey,\n remoteAccountId: remotePublicKey,\n localMnemonic,\n localDerivation,\n remoteMnemonic,\n remoteDerivation,\n };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, beforeEach, afterAll } = import.meta.vitest;\n const { mkdtemp, readFile, rm } = await import(\"node:fs/promises\");\n const { tmpdir } = await import(\"node:os\");\n const { fromHex } = await import(\"@polkadot-api/utils\");\n\n // Stable dev mnemonics — deterministic inputs keep the tests hermetic.\n // The first is the well-known Substrate dev root (the source of `//Alice`\n // et al.); the second is the BIP39 all-abandon test vector.\n const LOCAL_MNEMONIC = \"bottom drive obey lake curtain smoke basket hold race lonely fit walk\";\n const REMOTE_MNEMONIC =\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\";\n\n let storageDir: string;\n\n beforeEach(async () => {\n storageDir = await mkdtemp(join(tmpdir(), \"terminal-testing-\"));\n });\n\n afterAll(async () => {\n try {\n await rm(storageDir, { recursive: true });\n } catch {\n /* ignore */\n }\n });\n\n describe(\"createTestSession\", () => {\n test(\"writes both SsoSessions and UserSecrets files by default\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const sessions = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n expect(sessions).toMatch(/^0x[0-9a-f]+$/);\n\n const secrets = await readFile(\n join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`),\n \"utf-8\",\n );\n expect(secrets).toMatch(/^0x[0-9a-f]+$/);\n });\n\n test(\"omits UserSecrets file when includeSecrets is false\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n includeSecrets: false,\n });\n\n await expect(\n readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`), \"utf-8\"),\n ).rejects.toThrow(/ENOENT/);\n });\n\n test(\"SsoSessions file decodes with the host-papp session codec shape\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n sessionId: \"stable-test-id\",\n });\n\n const hex = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n const decoded = sessionsCodec.dec(fromHex(hex));\n\n expect(decoded).toHaveLength(1);\n expect(decoded[0].id).toBe(\"stable-test-id\");\n expect(decoded[0].localAccount.accountId).toEqual(result.localAccountId);\n expect(decoded[0].localAccount.pin).toBeUndefined();\n expect(decoded[0].remoteAccount.accountId).toEqual(result.remoteAccountId);\n expect(decoded[0].remoteAccount.publicKey).toHaveLength(32);\n expect(decoded[0].remoteAccount.pin).toBeUndefined();\n });\n\n test(\"localAccountId matches the Sr25519 public key derived from the mnemonic\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const expected = deriveSr25519PublicKey(\n createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), \"//wallet//sso\"),\n );\n expect(result.localAccountId).toEqual(expected);\n expect(result.localAccountId).toHaveLength(32);\n });\n\n test(\"UserSecrets file decrypts and decodes with the host-papp secret codec shape\", async () => {\n const appId = \"my-app\";\n const result = await createTestSession({\n appId,\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const hex = await readFile(\n join(storageDir, `${appId}_UserSecrets_${result.sessionId}.json`),\n \"utf-8\",\n );\n const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });\n const nonce = blake2b(new TextEncoder().encode(\"nonce\"), { dkLen: 32 });\n const decrypted = gcm(key, nonce).decrypt(fromHex(hex));\n const decoded = storedUserSecretsCodec.dec(decrypted);\n\n expect(decoded.entropy).toEqual(mnemonicToEntropy(LOCAL_MNEMONIC));\n // Sr25519 secret is 64 bytes (32-byte secret + 32-byte nonce).\n expect(decoded.ssSecret).toHaveLength(64);\n // P256 secret is 32 bytes.\n expect(decoded.encrSecret).toHaveLength(32);\n });\n\n test(\"different appIds produce files under different prefixes\", async () => {\n await createTestSession({ appId: \"app-a\", storageDir, sessionId: \"id\" });\n await createTestSession({ appId: \"app-b\", storageDir, sessionId: \"id\" });\n\n await expect(\n readFile(join(storageDir, \"app-a_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n await expect(\n readFile(join(storageDir, \"app-b_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"sanitizes appIds with special characters\", async () => {\n await createTestSession({\n appId: \"app/with spaces\",\n storageDir,\n sessionId: \"id\",\n });\n await expect(\n readFile(join(storageDir, \"app_with_spaces_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"creates storageDir when it does not yet exist\", async () => {\n const nested = join(storageDir, \"does\", \"not\", \"exist\");\n await createTestSession({ appId: \"my-app\", storageDir: nested });\n await expect(\n readFile(join(nested, \"my-app_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"generates fresh mnemonics when none are supplied\", async () => {\n const a = await createTestSession({ appId: \"a\", storageDir });\n const b = await createTestSession({ appId: \"b\", storageDir });\n expect(a.localMnemonic).not.toBe(b.localMnemonic);\n expect(a.remoteMnemonic).not.toBe(b.remoteMnemonic);\n expect(a.sessionId).not.toBe(b.sessionId);\n });\n\n test(\"respects an explicit sessionId\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"pinned-id\",\n });\n expect(result.sessionId).toBe(\"pinned-id\");\n await expect(\n readFile(join(storageDir, \"my-app_UserSecrets_pinned-id.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"respects a custom localDerivation\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n localDerivation: \"//custom//path\",\n });\n expect(result.localDerivation).toBe(\"//custom//path\");\n const expected = deriveSr25519PublicKey(\n createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), \"//custom//path\"),\n );\n expect(result.localAccountId).toEqual(expected);\n });\n\n test(\"repeated calls replace the session list\", async () => {\n // Documented behavior — second call leaves only the second session\n // on disk. Callers who want multiple sessions should use separate\n // storage dirs.\n const first = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"first\",\n });\n const second = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"second\",\n });\n\n const hex = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n const decoded = sessionsCodec.dec(fromHex(hex));\n expect(decoded).toHaveLength(1);\n expect(decoded[0].id).toBe(\"second\");\n // The first session's UserSecrets file is left behind (not cleaned).\n // This matches a real logout flow as well, so we don't try to hide it.\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${first.sessionId}.json`), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${second.sessionId}.json`), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"AccountIdCodec is Bytes(32) — keep this invariant if upstream changes\", () => {\n // Guards against silent upstream changes to the session-account\n // codec shape that would make our synthesized files unreadable.\n const encoded = AccountIdCodec.enc(createAccountId(new Uint8Array(32).fill(7)));\n expect(encoded).toHaveLength(32);\n });\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parity/product-sdk-terminal",
|
|
3
|
+
"description": "QR code login and signing for CLI/terminal apps via Polkadot mobile wallet pairing",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"./dist/register.js"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./register": {
|
|
18
|
+
"types": "./dist/register.d.ts",
|
|
19
|
+
"import": "./dist/register.js"
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"types": "./dist/testing.d.ts",
|
|
23
|
+
"import": "./dist/testing.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"scripts"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@noble/ciphers": "^2.1.0",
|
|
35
|
+
"@noble/curves": "^2.0.1",
|
|
36
|
+
"@noble/hashes": "^2.0.1",
|
|
37
|
+
"@novasamatech/host-papp": "^0.7.7",
|
|
38
|
+
"@novasamatech/statement-store": "^0.7.7",
|
|
39
|
+
"@novasamatech/storage-adapter": "^0.7.7",
|
|
40
|
+
"@polkadot-api/utils": "^0.4.0",
|
|
41
|
+
"@polkadot-api/ws-provider": "^0.9.0",
|
|
42
|
+
"@polkadot-labs/hdkd-helpers": "^0.0.27",
|
|
43
|
+
"nanoid": "^5.1.7",
|
|
44
|
+
"neverthrow": "^8.2.0",
|
|
45
|
+
"polkadot-api": "^2.1.2",
|
|
46
|
+
"qrcode": "^1.5.4",
|
|
47
|
+
"scale-ts": "^1.6.1",
|
|
48
|
+
"@parity/product-sdk-logger": "0.1.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/qrcode": "^1.5.5",
|
|
52
|
+
"tsup": "^8.4.0",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vitest": "^3.1.4"
|
|
55
|
+
},
|
|
56
|
+
"license": "Apache-2.0",
|
|
57
|
+
"scripts": {
|
|
58
|
+
"postinstall": "bash scripts/patch-wasm.sh",
|
|
59
|
+
"build": "tsup",
|
|
60
|
+
"clean": "rm -rf dist",
|
|
61
|
+
"pretest": "tsup",
|
|
62
|
+
"test": "vitest run"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Patch verifiablejs/bundler to load WASM via base64 embed instead of inline import.
|
|
3
|
+
# Required for bun runtime. Node.js/tsx users should use the ESM loader hook instead:
|
|
4
|
+
# --import @polkadot-apps/terminal/register
|
|
5
|
+
|
|
6
|
+
BUNDLER_JS=$(find node_modules -path "*/verifiablejs/pkg-bundler/verifiablejs.js" 2>/dev/null | head -1)
|
|
7
|
+
[ -z "$BUNDLER_JS" ] && exit 0
|
|
8
|
+
|
|
9
|
+
# Skip if already patched
|
|
10
|
+
head -1 "$BUNDLER_JS" | grep -q "__wbg_set_wasm" && exit 0
|
|
11
|
+
|
|
12
|
+
BUNDLER_DIR=$(dirname "$BUNDLER_JS")
|
|
13
|
+
# -w 0 disables line wrapping on Linux; -i is macOS flag
|
|
14
|
+
WASM_B64=$(base64 -w 0 "$BUNDLER_DIR/verifiablejs_bg.wasm" 2>/dev/null || base64 -i "$BUNDLER_DIR/verifiablejs_bg.wasm" 2>/dev/null || base64 "$BUNDLER_DIR/verifiablejs_bg.wasm")
|
|
15
|
+
|
|
16
|
+
cat > "$BUNDLER_JS" << SHIM
|
|
17
|
+
import { __wbg_set_wasm } from "./verifiablejs_bg.js";
|
|
18
|
+
import * as bg from "./verifiablejs_bg.js";
|
|
19
|
+
|
|
20
|
+
const wasmBytes = Uint8Array.from(atob("$WASM_B64"), c => c.charCodeAt(0));
|
|
21
|
+
const wasmModule = new WebAssembly.Module(wasmBytes);
|
|
22
|
+
const wasmInstance = new WebAssembly.Instance(wasmModule, { "./verifiablejs_bg.js": bg });
|
|
23
|
+
__wbg_set_wasm(wasmInstance.exports);
|
|
24
|
+
wasmInstance.exports.__wbindgen_start();
|
|
25
|
+
|
|
26
|
+
export * from "./verifiablejs_bg.js";
|
|
27
|
+
SHIM
|