@ghostlytools/cli 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/assets/{api-CrxVxtY1.js → api-Dq2QXQDk.js} +141 -40
- package/dist/assets/api-Dq2QXQDk.js.map +1 -0
- package/dist/assets/{api-BiaX6l6f.js → api-TiB5QSG9.js} +2 -2
- package/dist/assets/{apiKit-CajiDxdK.js → apiKit-CA2x5GJF.js} +7 -3
- package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
- package/dist/assets/{apps-CbfiCSIw.js → apps-BxV9i_Y4.js} +3 -4
- package/dist/assets/{apps-CbfiCSIw.js.map → apps-BxV9i_Y4.js.map} +1 -1
- package/dist/assets/{apps-t23gkc28.js → apps-C-T8jeji.js} +1 -1
- package/dist/assets/{arkade-D4BVJFEQ.js → arkade-C_qDmHnm.js} +2 -2
- package/dist/assets/{arkade-D4BVJFEQ.js.map → arkade-C_qDmHnm.js.map} +1 -1
- package/dist/assets/{backup-Cg52WeQi.js → backup-Bugnn3Ue.js} +1 -1
- package/dist/assets/{backup-BAMi2lAq.js → backup-CnMN2oUK.js} +8 -8
- package/dist/assets/backup-CnMN2oUK.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoind-CZchoh43.js +349 -0
- package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
- package/dist/assets/{buttons-CX1pKB0j.js → buttons-CvNBzh2S.js} +4 -5
- package/dist/assets/{buttons-CX1pKB0j.js.map → buttons-CvNBzh2S.js.map} +1 -1
- package/dist/assets/{endpoint-Dn7H8qE1.js → endpoint-BOTFMpfS.js} +44 -12
- package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
- package/dist/assets/{envelope-BvU0fLEt.js → envelope-WjingYSc.js} +2 -2
- package/dist/assets/{envelope-BvU0fLEt.js.map → envelope-WjingYSc.js.map} +1 -1
- package/dist/assets/{errors-my6VJR_0.js → errors-DO5z6h_l.js} +29 -1
- package/dist/assets/errors-DO5z6h_l.js.map +1 -0
- package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
- package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
- package/dist/assets/{host-Cmd205Dc.js → host-CWt-jtqz.js} +41 -16
- package/dist/assets/host-CWt-jtqz.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js → hyperdhtRelay-CPYwSLLS.js} +2 -3
- package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js.map → hyperdhtRelay-CPYwSLLS.js.map} +1 -1
- package/dist/assets/{irohWeb--YyEA-hX.js → irohWeb-4oChIOIp.js} +2 -3
- package/dist/assets/{irohWeb--YyEA-hX.js.map → irohWeb-4oChIOIp.js.map} +1 -1
- package/dist/assets/{main-BzNN6bkx.js → main-DbqcdShN.js} +142 -35
- package/dist/assets/main-DbqcdShN.js.map +1 -0
- package/dist/assets/{mainline-D5D5C-jT.js → mainline-ZkiOnevs.js} +2 -2
- package/dist/assets/{mainline-D5D5C-jT.js.map → mainline-ZkiOnevs.js.map} +1 -1
- package/dist/assets/{networkReason-DxZWHw6v.js → networkReason-CetKkTPo.js} +2 -2
- package/dist/assets/{networkReason-DxZWHw6v.js.map → networkReason-CetKkTPo.js.map} +1 -1
- package/dist/assets/nwc-CgkqyvsE.js +612 -0
- package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
- package/dist/assets/{profiles-B6F4oa9K.js → profiles-BHc9pEYg.js} +2 -2
- package/dist/assets/{profiles-B6F4oa9K.js.map → profiles-BHc9pEYg.js.map} +1 -1
- package/dist/assets/registry-DlAKO0Fy.js +3793 -0
- package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
- package/dist/assets/registry-OBJBnHVQ.js +2 -0
- package/dist/assets/resolve-CouHhCpJ.js +591 -0
- package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
- package/dist/assets/{server-CBCEYKMN.js → server-Ss0gJ-nw.js} +184 -1181
- package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
- package/dist/assets/{src-DhfGgAUu.js → src-DDzSERyc.js} +2249 -72
- package/dist/assets/src-DDzSERyc.js.map +1 -0
- package/dist/assets/{turnRefs-ChEfgbSX.js → turnRefs-B87dtrHM.js} +2 -2
- package/dist/assets/{turnRefs-ChEfgbSX.js.map → turnRefs-B87dtrHM.js.map} +1 -1
- package/dist/assets/types-C4_KafL_.js +122 -0
- package/dist/assets/types-C4_KafL_.js.map +1 -0
- package/dist/assets/{usdt-C1Xao2xH.js → usdt-Fj8CQO2z.js} +3 -3
- package/dist/assets/{usdt-C1Xao2xH.js.map → usdt-Fj8CQO2z.js.map} +1 -1
- package/dist/ghostly.mjs +1 -1
- package/package.json +1 -1
- package/dist/assets/api-CrxVxtY1.js.map +0 -1
- package/dist/assets/apiKit-CajiDxdK.js.map +0 -1
- package/dist/assets/backup-BAMi2lAq.js.map +0 -1
- package/dist/assets/buttons-CWl2yNJu.js +0 -2
- package/dist/assets/endpoint-Dn7H8qE1.js.map +0 -1
- package/dist/assets/errors-my6VJR_0.js.map +0 -1
- package/dist/assets/groupFiles-BNTSDaoq.js +0 -7685
- package/dist/assets/groupFiles-BNTSDaoq.js.map +0 -1
- package/dist/assets/groupFiles-DBbjOLuD.js +0 -2088
- package/dist/assets/groupFiles-DBbjOLuD.js.map +0 -1
- package/dist/assets/host-Cmd205Dc.js.map +0 -1
- package/dist/assets/main-BzNN6bkx.js.map +0 -1
- package/dist/assets/server-CBCEYKMN.js.map +0 -1
- package/dist/assets/src-DhfGgAUu.js.map +0 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as findChat, s as findGroup } from "./apiKit-
|
|
2
|
-
import { a as hideStateSecrets, i as groupSendRefused, n as SETTABLE, o as mentionsFor, r as callApi, s as redactSettings, t as API_METHODS } from "./api-
|
|
1
|
+
import { a as findChat, s as findGroup } from "./apiKit-CA2x5GJF.js";
|
|
2
|
+
import { a as hideStateSecrets, i as groupSendRefused, n as SETTABLE, o as mentionsFor, r as callApi, s as redactSettings, t as API_METHODS } from "./api-Dq2QXQDk.js";
|
|
3
3
|
export { API_METHODS, SETTABLE, callApi, findChat, findGroup, groupSendRefused, hideStateSecrets, mentionsFor, redactSettings };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Wn as INVITE_TAKEN } from "./src-DDzSERyc.js";
|
|
2
|
+
import { t as CliError } from "./errors-DO5z6h_l.js";
|
|
2
3
|
//#region src/apiKit.ts
|
|
3
4
|
var node = (ctx) => ctx.runtime.server.node;
|
|
4
5
|
var state = (ctx) => node(ctx).getState();
|
|
@@ -143,10 +144,13 @@ function findChatOrGroup(links, groups, ref) {
|
|
|
143
144
|
var chatOf = (ctx, params) => findChat(state(ctx).links, str(params, "chat", true));
|
|
144
145
|
/**
|
|
145
146
|
* A chat whose invite nobody took yet: nobody is there to pay or to dial, and its stage says so. Before, `chat pay`
|
|
146
|
-
* waited 15 s and `chat connect` 25 s for the contact, then answered `engine`.
|
|
147
|
+
* waited 15 s and `chat connect` 25 s for the contact, then answered `engine`. A chat whose invite someone else used
|
|
148
|
+
* first is refused too, with the sentence `send` gives: its contact seals everything to them (`connect` waited 120 s,
|
|
149
|
+
* then answered `timeout`, a code a bot retries).
|
|
147
150
|
*/
|
|
148
151
|
function inviteTaken(link) {
|
|
149
152
|
if (link.profile && link.inviteCode && !link.peerParticipationKey) throw new CliError("unavailable", "Nobody joined this chat yet: its invite is still waiting to be taken");
|
|
153
|
+
if (link.dhtDelivery?.inviteTaken && link.textDelivery !== "stream") throw new CliError("refused", INVITE_TAKEN);
|
|
150
154
|
}
|
|
151
155
|
/** A chat or a group as `findChatOrGroup` reads it. `linkId` is what the engine calls it. */
|
|
152
156
|
function chatOrGroup(ctx, ref) {
|
|
@@ -193,4 +197,4 @@ function waitForState(ctx, check, ms, what) {
|
|
|
193
197
|
//#endregion
|
|
194
198
|
export { str as _, findChat as a, groupOf as c, messageOf as d, node as f, state as g, sentAt as h, chatOrGroup as i, inviteTaken as l, oneOf as m, bool as n, findChatOrGroup as o, num as p, chatOf as r, findGroup as s, CONTACT_KEY as t, list as u, waitForState as v };
|
|
195
199
|
|
|
196
|
-
//# sourceMappingURL=apiKit-
|
|
200
|
+
//# sourceMappingURL=apiKit-CA2x5GJF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiKit-CA2x5GJF.js","names":[],"sources":["../../src/apiKit.ts"],"sourcesContent":["import { INVITE_TAKEN } from \"@ghostly/core\";\nimport type { GhostlyNode } from \"@ghostly/browser/engine/node\";\nimport type { EngineState, GroupView, LinkView, StoredMessage } from \"@ghostly/browser/shared/types\";\nimport { CliError } from \"./errors\";\nimport type { CallManager } from \"./calls/manager\";\nimport type { EventHub } from \"./events\";\nimport type { Runtime } from \"./runtime/engine\";\n\n/**\n * The methods of the local control API (WISP 1100): the daemon answers them over its socket, and a one-shot\n * command calls them in its own process. Parameters come from JSON: every one is checked here.\n */\nexport interface ApiContext {\n runtime: Runtime;\n hub: EventHub;\n /** Voice calls (WISP 1100 § Calls). */\n calls: CallManager;\n /** How the host runs: a daemon stays; a one-shot leaves when its command is done. */\n mode: \"daemon\" | \"one-shot\";\n version: string;\n /** The daemon asks to stop (answered first). */\n stop?: () => void;\n}\n\nexport type Params = Record<string, unknown>;\nexport type Method = (ctx: ApiContext, params: Params) => Promise<unknown>;\n\nexport const node = (ctx: ApiContext): GhostlyNode => ctx.runtime.server.node;\nexport const state = (ctx: ApiContext): EngineState => node(ctx).getState();\n\n/**\n * The time of a send the engine names its row after (`me_<timestamp>`: a file, a payment, a request), its own even\n * within one millisecond (a bot paying or sending several times at once over the socket, `file send` run in\n * parallel), or the chat keeps only one. One clock for all of them: they share the row names. A file's resend after a\n * drop finds its row by it, as a forward does.\n */\nlet lastSentAt = 0;\nexport const sentAt = () => (lastSentAt = Math.max(Date.now(), lastSentAt + 1));\n\n// ---------- parameters ----------\n\nexport function str(params: Params, name: string, required: true): string;\nexport function str(params: Params, name: string, required?: false): string | undefined;\nexport function str(params: Params, name: string, required = false): string | undefined {\n const value = params[name];\n if (value === undefined || value === null || value === \"\") {\n if (required) throw new CliError(\"bad_request\", `${name} is required`);\n return undefined;\n }\n if (typeof value !== \"string\") throw new CliError(\"bad_request\", `${name} must be a string`);\n return value;\n}\n/** A number in `[min, max]`; `whole` takes only whole ones (a count of messages), as the engine needs them. */\nexport function num(params: Params, name: string, fallback: number, { min = 0, max = Number.MAX_SAFE_INTEGER, whole = false } = {}): number {\n const value = params[name];\n if (value === undefined || value === null) return fallback;\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < min || value > max) throw new CliError(\"bad_request\", `${name} must be a ${whole ? \"whole \" : \"\"}number from ${min} to ${max}`);\n if (whole && !Number.isSafeInteger(value)) throw new CliError(\"bad_request\", `${name} must be a whole number from ${min} to ${max}`);\n return value;\n}\nexport function bool(params: Params, name: string): boolean {\n const value = params[name];\n if (value === undefined || value === null) return false;\n if (typeof value !== \"boolean\") throw new CliError(\"bad_request\", `${name} must be true or false`);\n return value;\n}\nexport function list(params: Params, name: string): string[] {\n const value = params[name];\n if (value === undefined || value === null) return [];\n if (!Array.isArray(value) || value.some((item) => typeof item !== \"string\")) throw new CliError(\"bad_request\", `${name} must be a list of strings`);\n return value as string[];\n}\nexport function oneOf<T extends string>(params: Params, name: string, values: readonly T[], fallback: T): T {\n const value = str(params, name) ?? fallback;\n if (!values.includes(value as T)) throw new CliError(\"bad_request\", `${name} must be one of ${values.join(\", \")}`);\n return value as T;\n}\n\n// ---------- finding things ----------\n\n/** A contact key as chats show it (`peer`): 52 characters of z-base-32. */\nexport const CONTACT_KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;\n\n/**\n * A chat by its id; by its contact's key (only by the key: a name is never taken for one); by a unique prefix of its\n * id; or by its name: the label this side gave it first, and only when no label matches, the name the contact gave\n * itself (which the contact controls, so it never wins over a label).\n */\nexport function findChat(links: readonly LinkView[], ref: string): LinkView {\n return chatBy(links, ref).link;\n}\n\n/** `findChat`, and whether only the name the contact gave itself (`theirName`) found the chat. */\nfunction chatBy(links: readonly LinkView[], ref: string): { link: LinkView; theirName: boolean } {\n const exact = links.find((link) => link.id === ref);\n if (exact) return { link: exact, theirName: false };\n if (CONTACT_KEY.test(ref)) {\n const byKey = links.filter((link) => link.peerPubKeyZ32 === ref);\n if (byKey.length === 1) return { link: byKey[0], theirName: false };\n if (byKey.length > 1) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: byKey.map((link) => link.id) });\n throw new CliError(\"not_found\", `No chat with ${JSON.stringify(ref)}`);\n }\n const byPrefix = links.filter((link) => link.id.startsWith(ref));\n if (byPrefix.length === 1) return { link: byPrefix[0], theirName: false };\n const lower = ref.toLowerCase();\n const named = (name: string | null | undefined) => name?.trim().toLowerCase() === lower;\n const byLabel = links.filter((link) => named(link.label));\n const byName = byLabel.length ? byLabel : links.filter((link) => named(link.peerNick));\n if (byName.length === 1) return { link: byName[0], theirName: !byLabel.length };\n if (byPrefix.length > 1 || byName.length > 1) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: [...byPrefix, ...byName].map((link) => link.id) });\n throw new CliError(\"not_found\", `No chat ${JSON.stringify(ref)}`);\n}\n\n/** Only an invitation so far: this profile is not in the group (yet), and whoever invited chose its name. */\nconst invitedOnly = (group: GroupView) => group.status === undefined && !!group.invitation;\n\n/**\n * A group by its id, by a unique prefix of its id, or by its name. A group this profile is in wins over an invitation\n * with the same prefix or name (the one who invites chooses those); an invitation is still found by its id, and by\n * prefix or name where no group of this profile has it.\n */\nexport function findGroup(groups: readonly GroupView[], ref: string): GroupView {\n const exact = groups.find((group) => group.id === ref);\n if (exact) return exact;\n const mineFirst = (found: GroupView[]) => { const mine = found.filter((group) => !invitedOnly(group)); return mine.length ? mine : found; };\n const byPrefix = mineFirst(groups.filter((group) => group.id.startsWith(ref)));\n if (byPrefix.length === 1) return byPrefix[0];\n const byName = mineFirst(groups.filter((group) => group.name.trim().toLowerCase() === ref.toLowerCase()));\n if (byName.length === 1) return byName[0];\n if (byPrefix.length > 1 || byName.length > 1) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names more than one group: use its id`, { matches: [...byPrefix, ...byName].map((group) => group.id) });\n throw new CliError(\"not_found\", `No group ${JSON.stringify(ref)}`);\n}\n\n/**\n * A chat or a group, where a command takes either: `group:<id>` is a group; anything else a chat first (`findChat`),\n * then a group. A chat found only by the name its contact gave itself never takes what also names a group: a group's\n * own id is that group, and a prefix or the name of a group this profile is in is refused, with both ids. With neither,\n * one `not_found` names both and where to look (a contact's key: no chat with it); a name that matches more than one\n * chat or group stays that error.\n */\nexport function findChatOrGroup(links: readonly LinkView[], groups: readonly GroupView[], ref: string): { link: LinkView } | { group: GroupView } {\n if (ref.startsWith(\"group:\")) return { group: findGroup(groups, ref.slice(\"group:\".length)) };\n let chat: ReturnType<typeof chatBy>;\n try {\n chat = chatBy(links, ref);\n } catch (error) {\n if (!(error instanceof CliError) || error.code !== \"not_found\") throw error;\n try { return { group: findGroup(groups, ref) }; } catch (inner) {\n if (!(inner instanceof CliError) || inner.code !== \"not_found\") throw inner;\n if (CONTACT_KEY.test(ref)) throw error;\n throw new CliError(\"not_found\", `No chat or group ${JSON.stringify(ref)} (ghostly chat list, ghostly group list)`);\n }\n }\n if (!chat.theirName) return { link: chat.link };\n const exact = groups.find((group) => group.id === ref);\n if (exact) return { group: exact };\n const lower = ref.toLowerCase();\n const also = groups.filter((group) => !invitedOnly(group) && (group.id.startsWith(ref) || group.name.trim().toLowerCase() === lower));\n if (also.length) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names a chat (the name its contact chose) and a group: use the chat's id or group:<id>`, { chat: chat.link.id, groups: also.map((group) => group.id) });\n return { link: chat.link };\n}\n\nexport const chatOf = (ctx: ApiContext, params: Params) => findChat(state(ctx).links, str(params, \"chat\", true));\n\n/**\n * A chat whose invite nobody took yet: nobody is there to pay or to dial, and its stage says so. Before, `chat pay`\n * waited 15 s and `chat connect` 25 s for the contact, then answered `engine`. A chat whose invite someone else used\n * first is refused too, with the sentence `send` gives: its contact seals everything to them (`connect` waited 120 s,\n * then answered `timeout`, a code a bot retries).\n */\nexport function inviteTaken(link: LinkView): void {\n if (link.profile && link.inviteCode && !link.peerParticipationKey) throw new CliError(\"unavailable\", \"Nobody joined this chat yet: its invite is still waiting to be taken\");\n if (link.dhtDelivery?.inviteTaken && link.textDelivery !== \"stream\") throw new CliError(\"refused\", INVITE_TAKEN);\n}\n\n/** A chat or a group as `findChatOrGroup` reads it. `linkId` is what the engine calls it. */\nexport function chatOrGroup(ctx: ApiContext, ref: string): { id: string; linkId: string; group: boolean } {\n const found = findChatOrGroup(state(ctx).links, state(ctx).groups, ref);\n return \"group\" in found ? { id: found.group.id, linkId: `group:${found.group.id}`, group: true } : { id: found.link.id, linkId: found.link.id, group: false };\n}\n\nexport const groupOf = (ctx: ApiContext, params: Params) => findGroup(state(ctx).groups, str(params, \"group\", true));\n\n/**\n * A message of a chat or a group (`group:<id>`) named by its id here, read as one row; only a name that is no such\n * row's key (the id both sides know, `wireId`) reads the whole history. `keep`: only a message it accepts (mine, or\n * someone else's). The history is synchronous work on the daemon's one thread, about 120 ms at 10,000 messages.\n */\nexport async function messageOf(ctx: ApiContext, linkId: string, ref: string, keep: (message: StoredMessage) => boolean = () => true): Promise<StoredMessage | undefined> {\n const found = await node(ctx).getMessage(linkId, ref);\n if (found && keep(found)) return found;\n const all = linkId.startsWith(\"group:\") ? await node(ctx).groupMessages({ groupId: linkId.slice(\"group:\".length) }) : await node(ctx).getMessages(linkId);\n return all.find((m) => keep(m) && m.wireId === ref);\n}\n\n// ---------- waiting ----------\n\n/** Resolves with `check`'s first defined answer: now, or on a later state; `timeout` error after `ms`. */\nexport function waitForState<T>(ctx: ApiContext, check: (state: EngineState) => T | undefined, ms: number, what: string): Promise<T> {\n const now = check(state(ctx));\n if (now !== undefined) return Promise.resolve(now);\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => { off(); reject(new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s waiting for ${what}`)); }, ms);\n const off = ctx.hub.onState((next) => {\n const value = check(next);\n if (value === undefined) return;\n clearTimeout(timer);\n off();\n resolve(value);\n });\n });\n}\n\n"],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as CliError } from "./errors-my6VJR_0.js";
|
|
1
|
+
import { Oc as seedSigner, _ as assembleAppBundle, _o as readAppRevocations, bo as canonicalJson, c as readAppStore, co as APP_PREFIXES, d as APP_BUNDLE_LIMITS, do as appRef, fo as checkAppRevokeStatement, gl as fromBase64Url, lo as APP_STATEMENT_LIMITS, n as appStoreDecision, s as checkAppStoreIndex, u as signAppStore, uo as appFingerprint, v as checkAppManifest, vl as toBase64Url, vo as signAppObject, x as readAppBundle, xo as canonicalJsonBytes, y as isAppUrl, yl as toZ32, yo as signAppRevocation } from "./src-DDzSERyc.js";
|
|
2
|
+
import { t as CliError } from "./errors-DO5z6h_l.js";
|
|
4
3
|
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
5
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import { createHash, createPrivateKey, generateKeyPairSync } from "node:crypto";
|
|
@@ -784,4 +783,4 @@ async function signStore(indexArg, keyArg, outArg, now = Math.floor(Date.now() /
|
|
|
784
783
|
//#endregion
|
|
785
784
|
export { STORE_SIG_FILE as a, publishApp as c, verifyApp as d, STORE_INDEX_FILE as i, revokeApp as l, APP_REVOKE_FILE as n, checkedBundle as o, APP_SOURCE as r, initApp as s, APP_BUNDLE_FILE as t, signStore as u };
|
|
786
785
|
|
|
787
|
-
//# sourceMappingURL=apps-
|
|
786
|
+
//# sourceMappingURL=apps-BxV9i_Y4.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps-CbfiCSIw.js","names":[],"sources":["../../src/appTemplate.ts","../../src/apps.ts"],"sourcesContent":["/*\n * What `ghostly app init` writes (WISP 1200, Apps · Publishing): the smallest app that does something with a contact.\n * Three files, each whole in this module, so the CLI's one built file carries them: the manifest as a publisher writes\n * it, a page that says hello to the same app on the contact's side, and a README with the way from here to a store.\n */\n\nexport interface AppTemplate { name: string; title: string }\n\nconst html = (text: string) => text.replace(/[&<>\"]/g, (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\" })[c]!);\n\n/** `ghostly-app.json`: the manifest without `publisher`, `sequence` and `files`, which `app publish` writes. */\nexport function templateManifest({ name, title }: AppTemplate): Record<string, unknown> {\n return {\n name,\n version: \"0.1.0\",\n kind: \"mini-app\",\n title,\n tagline: \"Say hello to a contact\",\n description: \"Open it in a chat and your contact gets a card to join. Each side can then say hello to the other.\",\n entry: \"index.html\",\n permissions: [\"chat\"],\n view: \"chat\",\n runtime: { host: \">=1.2\", clients: [\"web\", \"desktop\"] },\n license: \"MIT\",\n };\n}\n\n/**\n * `index.html`: the whole app in one file, as the sandbox needs it (no network, so nothing is loaded from elsewhere).\n * Its script is plain JavaScript typed with JSDoc against `@ghostlytools/sdk/app`, so it runs as written and still type\n * checks (test/appInit.test.ts does). The title is the only thing put into it, as text. Its type imports are in single\n * quotes: they are text of the page, not imports of the CLI's own build (test/packageDeps.test.ts reads those).\n */\nexport function templatePage({ title }: AppTemplate): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${html(title)}</title>\n<style>\n :root { color-scheme: light dark; --bg: #ffffff; --fg: #16181d; --muted: #5b6270; --line: #d9dde3; --accent: #2f6fed; }\n :root[data-theme=\"dark\"] { --bg: #16181d; --fg: #f2f4f7; --muted: #a3abb8; --line: #343a45; --accent: #7aa5ff; }\n body { margin: 0; padding: 20px; background: var(--bg); color: var(--fg); font: 16px/1.4 system-ui, sans-serif; }\n h1 { margin: 0 0 4px; font-size: 20px; }\n #status { margin: 0 0 16px; color: var(--muted); }\n button { font: inherit; padding: 8px 16px; border: 1px solid var(--accent); border-radius: 8px; background: var(--accent); color: var(--bg); }\n button:disabled { opacity: 0.5; }\n ul { margin: 16px 0 0; padding: 0; list-style: none; }\n li { padding: 8px 0; border-top: 1px solid var(--line); }\n</style>\n</head>\n<body>\n<h1>${html(title)}</h1>\n<p id=\"status\" role=\"status\">Starting</p>\n<button id=\"hello\" type=\"button\" disabled>Say hello</button>\n<ul id=\"log\" aria-label=\"Messages\"></ul>\n<script type=\"module\">\n// The types of the API come from the SDK (npm install --save-dev @ghostlytools/sdk); nothing of it runs here.\n/** @typedef {import('@ghostlytools/sdk/app').MiniAppApi} MiniAppApi */\n/** @typedef {import('@ghostlytools/sdk/app').MiniAppJson} MiniAppJson */\n\n/** The longest text shown from a contact's frame. */\nconst MAX_TEXT = 200;\n\nconst status = /** @type {HTMLElement} */ (document.getElementById(\"status\"));\nconst button = /** @type {HTMLButtonElement} */ (document.getElementById(\"hello\"));\nconst log = /** @type {HTMLElement} */ (document.getElementById(\"log\"));\n\n/** @param {string} text */\nfunction show(text) {\n const line = document.createElement(\"li\");\n line.textContent = text;\n log.append(line);\n}\n\n/**\n * What the contact's app sent, when it is a hello. A frame comes from another device: read only what this app sends,\n * and never put it in the page as HTML.\n * @param {MiniAppJson} data\n * @returns {string | null}\n */\nfunction helloOf(data) {\n if (!data || typeof data !== \"object\" || Array.isArray(data) || data.type !== \"hello\") return null;\n return typeof data.text === \"string\" && data.text.length <= MAX_TEXT ? data.text : null;\n}\n\n/**\n * Why a send was refused, in words. The code is the rejection's message (MINI_APP_ERROR_CODES in the SDK); a later\n * client may add one.\n * @param {unknown} error\n */\nfunction refusal(error) {\n switch (error instanceof Error ? error.message : \"\") {\n case \"offline\": return \"Not sent: you are not both online\";\n case \"peer-closed\": case \"not-open\": return \"Not sent: your contact has not opened this app\";\n case \"not-allowed\": return \"Open this app in a chat to say hello\";\n default: return \"Not sent\";\n }\n}\n\n/** @param {MiniAppApi} ghostly */\nasync function start(ghostly) {\n // Listen before anything is awaited, so no frame is missed.\n ghostly.chat.on(\"message\", (data) => {\n const text = helloOf(data);\n if (text !== null) show(\"Your contact: \" + text);\n });\n ghostly.chat.on(\"peer\", (peer) => {\n status.textContent = peer.open ? \"Your contact has this app open\" : \"Waiting for your contact to open this app\";\n });\n\n const context = await ghostly.context();\n const dark = context.theme ? context.theme === \"dark\" : matchMedia(\"(prefers-color-scheme: dark)\").matches;\n document.documentElement.dataset.theme = dark ? \"dark\" : \"light\";\n status.textContent = !context.inChat ? \"Open this app in a chat to say hello\"\n : context.peer ? \"Your contact has this app open\" : \"Waiting for your contact to open this app\";\n\n button.disabled = false;\n button.addEventListener(\"click\", async () => {\n try {\n await ghostly.chat.send({ type: \"hello\", text: \"Hello!\" });\n show(\"You: Hello!\");\n } catch (error) {\n show(refusal(error));\n }\n });\n}\n\n/** Ghostly's runner gives the app \\`window.ghostly\\`; a browser tab has none. */\nconst ghostly = /** @type {{ ghostly?: MiniAppApi }} */ (window).ghostly;\nif (ghostly) void start(ghostly);\nelse status.textContent = \"This page is a Ghostly app: publish it and open it in Ghostly\";\n</script>\n</body>\n</html>\n`;\n}\n\n/** `README.md`: what the folder holds and the way from it to a store. */\nexport function templateReadme({ name, title }: AppTemplate): string {\n return `# ${title}\n\nA [Ghostly](https://github.com/MiguelMedeiros/ghostly) app, started with \\`ghostly app init\\`. Opened in a 1:1 chat, it\ngives your contact a card to join, and each side can say hello to the other.\n\nA Ghostly app ([WISP 1200](https://github.com/MiguelMedeiros/ghostly/blob/dev/docs/wisps/1200-marketplace.md)) is a\npage that runs in the client's sandbox. It has no network, and talks to Ghostly only through \\`window.ghostly\\`:\nframes of at most 32 KiB to the same app on the contact's side (\\`chat.send\\`, \\`chat.on\\`), storage per app and per\nchat, and the files of its own bundle. Apps run in Ghostly from release 1.2.\n\n| File | What it is |\n|---|---|\n| \\`ghostly-app.json\\` | The manifest, without \\`publisher\\`, \\`sequence\\` and \\`files\\`: \\`ghostly app publish\\` writes those |\n| \\`index.html\\` | The app, whole: styles and script inline |\n| \\`README.md\\` | This file |\n\n## Write it\n\n\\`index.html\\` is plain JavaScript, typed with JSDoc against \\`@ghostlytools/sdk/app\\`: \\`MiniAppApi\\` is\n\\`window.ghostly\\`, and the same module has the limits (\\`MINI_APP_LIMITS\\`) and the codes a refused call rejects with\n(\\`MINI_APP_ERROR_CODES\\`). For the types in your editor:\n\n\\`\\`\\`sh\nnpm install --save-dev @ghostlytools/sdk\n\\`\\`\\`\n\n- What a contact's app sends is untrusted: check its shape, bound its size, and show it as text, never as HTML.\n- \\`\"view\": \"chat\"\\` runs the app inside a chat with one contact. \\`\"full\"\\` is a full-screen app opened from the Apps page.\n- \\`permissions\\` lists what the person is asked for: \\`chat\\` (frames to the contact's app), \\`name\\` (the person's\n display name) and \\`internet\\`. Adding one later makes every install ask before it updates.\n\n## Build\n\nThere is nothing to build yet: the page is the app. Once it grows into several source files, or TypeScript, build it\ninto one self-contained \\`index.html\\` (scripts, styles and images inline) and put that file and \\`ghostly-app.json\\`\nalone in a folder of their own, which is the folder you publish.\n[ghostly-chess](https://github.com/MiguelMedeiros/ghostly-chess) does it with Vite and a small\n\\`scripts/stage.mjs\\`. \\`ghostly app publish\\` takes every file of the folder it is given but dot files, this README\nincluded, so do not run it on a repository with sources, \\`node_modules\\` or anything private in it.\n\n## Publish\n\n\\`\\`\\`sh\nghostly app publish . --key ~/ghostly-keys/${name}-publisher.key\nghostly app verify app.ghostlyapp\n\\`\\`\\`\n\n\\`publish\\` signs the folder into \\`app.ghostlyapp\\`. The first run makes the publisher key at the path you name:\n\n- Keep it **outside this folder** and its repository (\\`publish\\` refuses a key inside it), and never commit it.\n- Back it up. An app is updated only with the key that signed it; a lost key means a new app.\n\n\\`verify\\` checks the bundle as Ghostly will and prints its \\`ref\\` (\\`<publisher key>/${name}\\`), \\`sequence\\` and\n\\`digest\\`. For the next version, raise \\`version\\` in \\`ghostly-app.json\\` and publish again: the sequence goes up\nby one by itself.\n\n## Try it\n\nCommit \\`app.ghostlyapp\\` to a public GitHub repository, then in Ghostly open Apps, choose Add, and paste its URL\n(\\`https://raw.githubusercontent.com/<owner>/<repo>/HEAD/app.ghostlyapp\\`). Set \\`sources\\` in \\`ghostly-app.json\\`\nto that URL, in a list, so installs made from it find your updates. Open the app from a chat's + menu; your contact\ngets a card to install and open it.\n\n## List it in a store\n\nA store is a signed list of apps. The official one is a Git repository,\n[ghostly-store](https://github.com/MiguelMedeiros/ghostly-store), and apps enter it by pull request:\n\n1. Commit \\`app.ghostlyapp\\` to your public repository and note that commit.\n2. Add \\`apps/${name}.<first 16 characters of your publisher key>/listing.json\\` with the \\`ref\\`, \\`sequence\\`\n and \\`digest\\` that \\`ghostly app verify\\` printed. Its \\`urls\\` start with a jsDelivr URL pinned to the full commit\n (\\`https://cdn.jsdelivr.net/gh/<owner>/<repo>@<40-character commit>/app.ghostlyapp\\`).\n3. Open the pull request. Its check verifies the bundle; a maintainer reviews the app and the owner signs the index.\n\nThe store's [CONTRIBUTING](https://github.com/MiguelMedeiros/ghostly-store/blob/main/CONTRIBUTING.md) has the rules.\nAn app installed from a store updates only to the version that store lists, so each new version is a pull request too.\n\n## If something goes wrong\n\nIf a version is harmful, \\`ghostly app revoke . --key <your key> --up-to <sequence>\\` writes \\`ghostly-revoke.json\\`;\npublish it beside \\`app.ghostlyapp\\` and Ghostly stops those versions.\n`;\n}\n","import { createHash, createPrivateKey, generateKeyPairSync } from \"node:crypto\";\nimport { closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, writeSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport {\n APP_BUNDLE_LIMITS, APP_PREFIXES, appFingerprint, appRef, appStoreDecision, assembleAppBundle, canonicalJson, canonicalJsonBytes,\n APP_STATEMENT_LIMITS, checkAppManifest, checkAppRevokeStatement, checkAppStoreIndex, fromBase64Url, isAppUrl, readAppBundle,\n readAppRevocations, readAppStore, seedSigner, signAppObject, signAppRevocation, signAppStore, toBase64Url, toZ32, type AppBundle,\n type AppManifest, type AppRevokeStatement, type AppStoreIndex, type SignedAppRevocation, type Signer,\n} from \"@ghostly/core\";\nimport { templateManifest, templatePage, templateReadme } from \"./appTemplate\";\nimport { CliError } from \"./errors\";\n\n/*\n * The publisher tools of WISP 1200 (Apps · Publishing): `app init` writes a folder to start from, `app publish` bundles\n * and signs a folder, `app verify` checks a bundle as a client would, `store sign` signs a store index. They run here, on files: no profile, no daemon, no\n * network but the one read `app verify` makes of a URL.\n *\n * Keys. A publisher key and a store key are Ed25519 keys of their own, never a profile's (WISP 1200 · Publisher\n * identity). Each lives in a file the command names with `--key`: a PKCS #8 PEM block (what `openssl genpkey -algorithm\n * ed25519` writes, so secret scanners know it for a private key), after one line that says which kind it is. The file\n * is made owner-only (0600) when a command makes it, and refused when anyone else may read it. A key is never printed:\n * only its public key and fingerprint are.\n */\n\nexport const APP_SOURCE = \"ghostly-app.json\";\nexport const APP_BUNDLE_FILE = \"app.ghostlyapp\";\nexport const APP_REVOKE_FILE = \"ghostly-revoke.json\";\nexport const STORE_INDEX_FILE = \"ghostly-store.json\";\nexport const STORE_SIG_FILE = \"ghostly-store.sig\";\n\n/** How long `app verify` waits for a URL, and how many redirects it follows (each checked as the first). */\nconst FETCH_TIMEOUT_MS = 60_000;\nconst MAX_REDIRECTS = 5;\n\n// ---------- keys ----------\n\nexport type KeyKind = \"publisher\" | \"store\";\n\nconst KEY_HEADER: Record<KeyKind, string> = {\n publisher: \"Ghostly publisher key (WISP 1200). Secret: never share or commit it. Back it up: an app signed with it is updated only with it.\",\n store: \"Ghostly store key (WISP 1200). Secret: keep it offline, never share or commit it. Back it up: a store is its key.\",\n};\nconst PEM = /-----BEGIN PRIVATE KEY-----\\r?\\n([A-Za-z0-9+/=\\r\\n]+)-----END PRIVATE KEY-----/;\n\nexport interface LoadedKey { signer: Signer; key: string; created: boolean }\n\n/** Refuses a key file that is not a plain file of this user's, or that anyone else may read or write. */\nfunction checkKeyFile(path: string): void {\n const info = statSync(path);\n if (!info.isFile()) throw new CliError(\"bad_request\", `${path} is not a file: --key names a key file`);\n if (process.platform === \"win32\") return;\n if (process.getuid && info.uid !== process.getuid()) throw new CliError(\"refused\", `${path} is not this user's: a key file must be yours alone`);\n if ((info.mode & 0o077) !== 0) {\n throw new CliError(\"refused\", `${path} may be read by others (mode ${(info.mode & 0o777).toString(8)}): run chmod 600 ${path}, and think whether the key was seen`);\n }\n}\n\n/** Reads a key file: the PEM block, an Ed25519 key, and the kind its first line names (when it names one). */\nfunction readKey(path: string, kind: KeyKind): LoadedKey {\n checkKeyFile(path);\n const text = readFileSync(path, \"utf8\");\n const other: KeyKind = kind === \"publisher\" ? \"store\" : \"publisher\";\n if (text.startsWith(`Ghostly ${other} key`)) {\n throw new CliError(\"refused\", `${path} is a ${other} key: ${kind === \"publisher\" ? \"a store key never signs an app\" : \"a publisher key never signs a store index\"} (WISP 1200 · Stores)`);\n }\n const block = PEM.exec(text);\n let seed: Uint8Array | undefined;\n if (block) {\n try {\n const key = createPrivateKey(block[0]);\n const d = key.asymmetricKeyType === \"ed25519\" ? key.export({ format: \"jwk\" }).d : undefined;\n if (d) seed = fromBase64Url(d);\n } catch { /* said below, without the file's text */ }\n }\n if (!seed || seed.length !== 32) throw new CliError(\"bad_request\", `${path} holds no Ed25519 private key (a PEM \"PRIVATE KEY\" block, as openssl genpkey -algorithm ed25519 writes)`);\n const signer = seedSigner(seed);\n return { signer, key: toZ32(signer.publicKey), created: false };\n}\n\n/** Makes a key file: owner-only from the start, never over a file that is there. */\nfunction makeKey(path: string, kind: KeyKind): LoadedKey {\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n const { privateKey } = generateKeyPairSync(\"ed25519\");\n const pem = privateKey.export({ type: \"pkcs8\", format: \"pem\" }) as string;\n const fd = openSync(path, \"wx\", 0o600);\n try { writeSync(fd, `${KEY_HEADER[kind]}\\n${pem}`); } finally { closeSync(fd); }\n return { ...readKey(path, kind), created: true };\n}\n\n/** What the person is told on stderr when a key was made: where, which, and to back it up. Never the key. */\nfunction madeKeyNote(path: string, kind: KeyKind, key: string): string {\n return kind === \"publisher\"\n ? `new publisher key written to ${path}; back it up (${appFingerprint(key)}: an app is updated only with the key that signed it, and phase 1 has no recovery)`\n : `new store key written to ${path}; back it up and keep it offline (${appFingerprint(key)}: a store is its key)`;\n}\n\n// ---------- reading bundles ----------\n\nconst refused = (what: string, reading: { reason: string; detail?: string }, extra: Record<string, unknown> = {}) =>\n new CliError(\"refused\", `${what} (${reading.reason}${reading.detail ? `: ${reading.detail}` : \"\"})`, { reason: reading.reason, ...(reading.detail ? { detail: reading.detail } : {}), ...extra });\n\n/** A bundle file's bytes: its size is checked before it is read, so a huge file is refused unread. */\nfunction readBundleFile(path: string): Uint8Array {\n const info = statSync(path);\n if (info.isDirectory()) throw new CliError(\"bad_request\", `${path} is a folder: name a .ghostlyapp file`);\n if (info.size > APP_BUNDLE_LIMITS.bundleBytes) throw refused(`${path} is not a valid bundle`, { reason: \"too-large\", detail: `${info.size} bytes` });\n return new Uint8Array(readFileSync(path));\n}\n\n/**\n * A bundle at an https URL, as a client fetches one: `https:` only, no user or password, jsDelivr only at a full\n * commit (WISP 1200 · Manifest, \"Every URL\"); every redirect is held to the same rule, and the download stops past\n * 16 MiB.\n */\nasync function fetchBundle(url: string): Promise<Uint8Array> {\n let at = url;\n for (let hop = 0; ; hop++) {\n if (!isAppUrl(at)) {\n throw new CliError(hop ? \"refused\" : \"usage\", hop\n ? `${url} redirects to ${at}, which a client does not read (https only, no user or password, jsDelivr only at a full commit)`\n : `Not a URL a client reads: https only, at most ${APP_BUNDLE_LIMITS.url} characters, no user or password, and jsDelivr only as https://cdn.jsdelivr.net/gh/<owner>/<repo>@<40-hex commit>/<path>`);\n }\n let response: Response;\n try {\n response = await fetch(at, { redirect: \"manual\", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });\n } catch (error) {\n if (error instanceof Error && error.name === \"TimeoutError\") throw new CliError(\"timeout\", `${at} did not answer in ${FETCH_TIMEOUT_MS / 1000} s`);\n throw new CliError(\"unavailable\", `Could not read ${at}: ${error instanceof Error ? error.message : String(error)}`);\n }\n if (response.status >= 300 && response.status < 400 && response.headers.get(\"location\")) {\n await response.body?.cancel();\n if (hop >= MAX_REDIRECTS) throw new CliError(\"refused\", `${url} redirects more than ${MAX_REDIRECTS} times`);\n at = new URL(response.headers.get(\"location\")!, at).href;\n continue;\n }\n if (response.status === 404 || response.status === 410) { await response.body?.cancel(); throw new CliError(\"not_found\", `${at} answered ${response.status}`); }\n if (!response.ok || !response.body) { await response.body?.cancel(); throw new CliError(\"unavailable\", `${at} answered ${response.status}`); }\n const limit = APP_BUNDLE_LIMITS.bundleBytes;\n const tooLarge = (bytes: number | string) => refused(`${url} is not a valid bundle`, { reason: \"too-large\", detail: `${bytes} bytes` });\n const length = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(length) && length > limit) { await response.body.cancel(); throw tooLarge(length); }\n const chunks: Uint8Array[] = [];\n let total = 0;\n const reader = response.body.getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.length;\n if (total > limit) { await reader.cancel(); throw tooLarge(`more than ${limit}`); }\n chunks.push(value);\n }\n } catch (error) {\n if (error instanceof CliError) throw error;\n if (error instanceof Error && error.name === \"TimeoutError\") throw new CliError(\"timeout\", `${at} did not finish in ${FETCH_TIMEOUT_MS / 1000} s`);\n throw new CliError(\"unavailable\", `Could not read ${at}: ${error instanceof Error ? error.message : String(error)}`);\n }\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) { out.set(chunk, offset); offset += chunk.length; }\n return out;\n }\n}\n\n/** What a checked bundle is, as `app verify` and `app publish` print it. */\nfunction described(bundle: AppBundle, bytes: Uint8Array) {\n const m = bundle.manifest;\n return {\n ref: appRef(m.publisher, m.name), publisher: m.publisher, fingerprint: appFingerprint(m.publisher), name: m.name,\n title: m.title, version: m.version, sequence: m.sequence, digest: bundle.digest, bytes: bytes.length,\n sha256: toBase64Url(createHash(\"sha256\").update(bytes).digest()),\n };\n}\n\n/** `app verify <bundle|url>`: every check a client makes before it stores a bundle (WISP 1200 · Reading a bundle). */\nexport async function verifyApp(source: string): Promise<Record<string, unknown>> {\n const { bundle, bytes } = await checkedBundle(source);\n return { valid: true, ...described(bundle, bytes), manifest: bundle.manifest };\n}\n\n/** A bundle from a file or an https URL, with every check of `app verify`; `url` when it came from one. */\nexport async function checkedBundle(source: string): Promise<{ bundle: AppBundle; bytes: Uint8Array; url?: string }> {\n const isUrl = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(source);\n const bytes = isUrl ? await fetchBundle(source) : readBundleFile(resolve(source));\n const read = readAppBundle(bytes);\n if (!read.ok) throw refused(`${source} is not a valid bundle`, read);\n return { bundle: read.bundle, bytes, ...(isUrl && { url: source }) };\n}\n\n// ---------- starting an app ----------\n\n/** A folder's name as an app's: lower case, anything else a hyphen (`My App` is `my-app`). */\nconst nameFrom = (folder: string) => folder.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n/** An app's name as a title: `my-app` is `My app`. */\nconst titleFrom = (name: string) => (name[0]!.toUpperCase() + name.slice(1)).replace(/-/g, \" \");\n\n/**\n * `app init <dir> [--name <name>] [--title <title>] [--force]`: a small working app in `<dir>` (made when missing),\n * which `app publish <dir>` signs as it is: `ghostly-app.json`, an `index.html` that says hello to the contact, and a\n * README with the way to a store (./appTemplate.ts). The name is `--name`, else the folder's; the title `--title`, else\n * the name's. Nothing is written when one of the three files is there, unless `--force`, which replaces those three only.\n */\nexport async function initApp(dirArg: string, what: { name?: string; title?: string; force?: boolean } = {}): Promise<Record<string, unknown>> {\n const usage = \"ghostly app init <dir> [--name <name>] [--title <title>] [--force]\";\n const dir = resolve(dirArg);\n const name = what.name ?? nameFrom(basename(dir));\n const title = what.title ?? (name ? titleFrom(name) : \"\");\n const page = templatePage({ name, title });\n // Checked as publish will check it, under a key of zeros and the page's own entry: what is written always publishes.\n const pageBytes = new TextEncoder().encode(page);\n const checked = checkAppManifest({\n ...templateManifest({ name, title }), ghostlyApp: 1, publisher: toZ32(new Uint8Array(32)), sequence: 1,\n files: [{ path: \"index.html\", size: pageBytes.length, sha256: toBase64Url(createHash(\"sha256\").update(pageBytes).digest()) }],\n }, { strict: true });\n if (!checked.ok) {\n if (checked.detail === \"name is not valid\") {\n throw new CliError(\"usage\", what.name === undefined\n ? `The folder's name gives no app name (${JSON.stringify(basename(dir))}): give --name, a lowercase letter then up to 31 lowercase letters, digits or hyphens: ${usage}`\n : `--name is a lowercase letter then up to 31 lowercase letters, digits or hyphens, not ${JSON.stringify(what.name)}`);\n }\n if (checked.detail === \"title is not valid\") throw new CliError(\"usage\", `--title is one line of 1 to ${APP_BUNDLE_LIMITS.title} characters, not ${JSON.stringify(title)}`);\n throw refused(\"The template makes no valid manifest\", checked);\n }\n\n if (existsSync(dir) && !statSync(dir).isDirectory()) throw new CliError(\"bad_request\", `${dir} is a file: name a folder for the app`);\n const files: [string, string][] = [\n [APP_SOURCE, `${JSON.stringify(templateManifest({ name, title }), null, 2)}\\n`],\n [\"index.html\", page],\n [\"README.md\", templateReadme({ name, title })],\n ];\n const held = files.map(([path]) => path).filter((path) => existsSync(join(dir, path)));\n if (held.length && !what.force) {\n throw refused(`${dir} already holds ${held.join(\", \")}: nothing was written (--force replaces ${held.length > 1 ? \"them\" : \"it\"})`, { reason: \"exists\" }, { files: held });\n }\n mkdirSync(dir, { recursive: true });\n for (const [path, text] of files) writeWhole(join(dir, path), new TextEncoder().encode(text));\n return {\n dir, name, title, files: files.map(([path]) => path), ...(held.length ? { replaced: held } : {}),\n next: `ghostly app publish ${dirArg} --key <a key file outside ${dirArg}>`,\n };\n}\n\n// ---------- publishing ----------\n\n/** Every file of the folder, as the bundle names it; dot files and folders are left out and listed. */\nfunction collectFiles(dir: string, leaveOut: Set<string>): { files: { path: string; full: string; size: number }[]; skipped: string[] } {\n const files: { path: string; full: string; size: number }[] = [];\n const skipped: string[] = [];\n let bytes = 0;\n const walk = (folder: string) => {\n for (const name of readdirSync(folder).sort()) {\n const full = join(folder, name);\n const path = relative(dir, full).split(sep).join(\"/\");\n if (leaveOut.has(full)) continue;\n if (name.startsWith(\".\")) { skipped.push(path); continue; }\n const info = lstatSync(full);\n if (info.isSymbolicLink()) throw new CliError(\"bad_request\", `${path} is a link: a bundle holds the folder's own files (copy it in, or remove it)`);\n if (info.isDirectory()) { walk(full); continue; }\n if (!info.isFile()) throw new CliError(\"bad_request\", `${path} is not a file`);\n files.push({ path, full, size: info.size });\n bytes += info.size;\n if (files.length > APP_BUNDLE_LIMITS.files) throw refused(`${dir} holds more files than a bundle may`, { reason: \"too-many-files\", detail: `more than ${APP_BUNDLE_LIMITS.files}` });\n if (bytes > APP_BUNDLE_LIMITS.bundleBytes) throw refused(`${dir} holds more than a bundle may`, { reason: \"too-large\", detail: `more than ${APP_BUNDLE_LIMITS.bundleBytes} bytes of files` });\n }\n };\n walk(dir);\n // The manifest lists every path in code-unit order, across folders: `lib-extra.js` before `lib/x.js`, which the walk\n // reaches first (`-` and `.` sort before `/`).\n files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n return { files, skipped };\n}\n\n/**\n * A private key in a file: a PEM private key block with its body (PKCS #8, OpenSSH, RSA, EC, encrypted), or a Ghostly\n * key file's first line. A library that only names the block's header in its code does not match.\n */\nconst PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----\\r?\\n[A-Za-z0-9+/=\\r\\n:,-]{40,}-----END [A-Z0-9 ]*PRIVATE KEY-----|^Ghostly (publisher|store) key \\(WISP 1200\\)/;\n\n/** Whether `path` is `dir` or anything under it. */\nconst isInside = (dir: string, path: string) => { const rel = relative(dir, path); return !rel.startsWith(\"..\") && !isAbsolute(rel); };\n\n/** Fields of `ghostly-app.json` the command writes itself. */\nconst WRITTEN = [\"publisher\", \"files\"] as const;\n\nfunction readSource(path: string): Record<string, unknown> {\n if (!existsSync(path)) throw new CliError(\"not_found\", `No ${APP_SOURCE} in ${dirname(path)}: it holds the manifest's fields (name, version, title, tagline, entry, permissions, runtime, license, ...) but publisher, sequence and files, which publish writes`);\n let value: unknown;\n try { value = JSON.parse(readFileSync(path, \"utf8\")); } catch (error) { throw new CliError(\"bad_request\", `${path} is not JSON: ${error instanceof Error ? error.message : String(error)}`); }\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new CliError(\"bad_request\", `${path} is not a JSON object`);\n const draft = value as Record<string, unknown>;\n const written = WRITTEN.filter((k) => k in draft);\n if (written.length) throw new CliError(\"bad_request\", `${path} has ${written.join(\" and \")}: publish writes ${written.length > 1 ? \"them\" : \"it\"} from the key and the folder's files`);\n if (draft.ghostlyApp !== undefined && draft.ghostlyApp !== 1) throw refused(`${path} is not a manifest this release makes`, { reason: \"unsupported-format\", detail: \"ghostlyApp is not 1\" });\n if (draft.sequence !== undefined && !(Number.isSafeInteger(draft.sequence) && (draft.sequence as number) >= 1)) {\n throw new CliError(\"bad_request\", `${path}: sequence is a whole number from 1 (or leave it out: publish raises it by itself)`);\n }\n return draft;\n}\n\n/** Writes a file whole or not at all: a reader never sees half a bundle (and a dot file left by a crash is never bundled). */\nfunction writeWhole(path: string, bytes: Uint8Array): void {\n const tmp = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);\n writeFileSync(tmp, bytes);\n renameSync(tmp, path);\n}\n\n/**\n * `app publish <dir> --key <file> [--out <file>]`: `<dir>/ghostly-app.json` (the manifest without `publisher`,\n * `sequence` and `files`) and every other file in `<dir>` become one signed bundle, by default `<dir>/app.ghostlyapp`.\n * The sequence is `--sequence` (higher than the bundle already at `--out`'s), else one more than that bundle's (or\n * the source's `sequence`, if higher, else 1). The\n * key file is made when it is missing and no earlier bundle is there to update.\n */\nexport async function publishApp(dirArg: string, keyArg: string, outArg?: string, sequenceArg?: number): Promise<Record<string, unknown>> {\n if (sequenceArg !== undefined && !(Number.isSafeInteger(sequenceArg) && sequenceArg >= 1)) throw new CliError(\"usage\", `--sequence takes a whole number from 1, not ${sequenceArg}`);\n const dir = resolve(dirArg);\n if (!statSync(dir).isDirectory()) throw new CliError(\"bad_request\", `${dir} is not a folder`);\n const keyPath = resolve(keyArg);\n // Every file of the folder is published, and the folder is often the repository the bundle is committed to.\n if (isInside(dir, keyPath)) {\n throw refused(`${keyPath} is inside ${dir}, which is published: keep the publisher key outside the app's folder (and its repository)`, { reason: \"private-key\" });\n }\n const out = resolve(outArg ?? join(dir, APP_BUNDLE_FILE));\n const sourcePath = join(dir, APP_SOURCE);\n const draft = readSource(sourcePath);\n\n // The version this one follows: the bundle already at the output, which only its own key may follow.\n let previous: AppManifest | null = null;\n if (existsSync(out)) {\n const read = readAppBundle(readBundleFile(out));\n if (!read.ok) throw refused(`${out} is there and is not a valid bundle: move it away, or name another --out`, read);\n previous = read.bundle.manifest;\n }\n\n let key: LoadedKey;\n if (existsSync(keyPath)) key = readKey(keyPath, \"publisher\");\n else if (previous) {\n throw new CliError(\"not_found\", `No key file ${keyPath}: ${out} is signed by ${appFingerprint(previous.publisher)}, and only that key publishes its next version`);\n } else key = makeKey(keyPath, \"publisher\");\n\n if (previous && previous.publisher !== key.key) {\n throw refused(`${out} is ${appRef(previous.publisher, previous.name)}, signed by another key (${appFingerprint(previous.publisher)}): a version of an app is signed by its own key`, { reason: \"other-app\" }, { publisher: previous.publisher });\n }\n if (previous && previous.name !== draft.name) {\n throw refused(`${out} is the app ${JSON.stringify(previous.name)}, not ${JSON.stringify(draft.name)}: name another --out for another app`, { reason: \"other-app\" });\n }\n\n if (sequenceArg !== undefined && previous && sequenceArg <= previous.sequence) {\n throw refused(`--sequence ${sequenceArg} is not higher than the ${previous.sequence} of ${out}: every client refuses a lower one, and the same one with other content is equivocation`, { reason: sequenceArg < previous.sequence ? \"rollback\" : \"equivocation\" }, { previous: previous.sequence });\n }\n const sequence = sequenceArg ?? Math.max(previous ? previous.sequence + 1 : 1, (draft.sequence as number | undefined) ?? 1);\n const { files, skipped } = collectFiles(dir, new Set([sourcePath, out, join(dir, APP_REVOKE_FILE)]));\n const contents = files.map((f) => ({ path: f.path, bytes: new Uint8Array(readFileSync(f.full)) }));\n // A bundle is public: a private key found in the folder (an old key, a store key, any PEM) stops the publish.\n const keyFile = contents.find((f) => PRIVATE_KEY.test(Buffer.from(f.bytes).toString(\"latin1\")));\n if (keyFile) throw refused(`${keyFile.path} in ${dir} holds a private key, and a bundle is public: move it out of the app's folder`, { reason: \"private-key\", detail: keyFile.path });\n const manifest = {\n ...draft,\n ghostlyApp: 1,\n publisher: key.key,\n sequence,\n files: contents.map((f) => ({ path: f.path, size: f.bytes.length, sha256: toBase64Url(createHash(\"sha256\").update(f.bytes).digest()) })),\n };\n const checked = checkAppManifest(JSON.parse(JSON.stringify(manifest)), { strict: true });\n if (!checked.ok) throw refused(`${sourcePath} and the folder's files make no valid manifest`, checked);\n\n const { bytes: manifestBytes, signature } = await signAppObject(APP_PREFIXES.app, checked.manifest, key.signer);\n const bundle = assembleAppBundle(manifestBytes, canonicalJsonBytes(signature), contents.map((f) => f.bytes));\n // Read back as a client would (the icon's header, the bundle's size): what is written is never a bundle it refuses.\n const read = readAppBundle(bundle);\n if (!read.ok) throw refused(\"The bundle made is not valid\", read);\n mkdirSync(dirname(out), { recursive: true });\n writeWhole(out, bundle);\n\n return {\n ...described(read.bundle, bundle),\n previous: previous?.sequence ?? null,\n out,\n files: contents.map((f) => f.path),\n ...(skipped.length ? { skipped } : {}),\n keyCreated: key.created,\n ...(key.created ? { warning: madeKeyNote(keyPath, \"publisher\", key.key) } : {}),\n };\n}\n\n// ---------- revocations ----------\n\n/**\n * `app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>] [--bundle <file>]`: a\n * `ghostly-revoke/1` statement (WISP 1200 · Publisher keys, Revocation) for the app whose bundle is `<dir>/app.ghostlyapp`\n * (or `--bundle`), signed by that app's publisher key and added to `<dir>/ghostly-revoke.json`, which stays canonical.\n * The key is never made here: only the key that signed the app may revoke its versions.\n */\nexport async function revokeApp(dirArg: string, keyArg: string, what: { digests?: string[]; upTo?: number; reason?: string; bundle?: string }): Promise<Record<string, unknown>> {\n const dir = resolve(dirArg);\n const bundlePath = resolve(what.bundle ?? join(dir, APP_BUNDLE_FILE));\n const listPath = join(dir, APP_REVOKE_FILE);\n const usage = \"ghostly app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>]\";\n const digests = what.digests ?? [];\n if ((digests.length > 0) === (what.upTo !== undefined)) throw new CliError(\"usage\", `Give --digest (again for each) or --up-to, one of the two: ${usage}`);\n if (what.upTo !== undefined && !(Number.isSafeInteger(what.upTo) && what.upTo >= 1)) throw new CliError(\"usage\", `--up-to takes a sequence, a whole number from 1, not ${what.upTo}`);\n\n if (!existsSync(bundlePath)) throw new CliError(\"not_found\", `No bundle ${bundlePath}: a revocation names the app of the bundle in <dir> (or --bundle)`);\n const read = readAppBundle(readBundleFile(bundlePath));\n if (!read.ok) throw refused(`${bundlePath} is not a valid bundle`, read);\n const manifest = read.bundle.manifest;\n const ref = appRef(manifest.publisher, manifest.name);\n\n const keyPath = resolve(keyArg);\n if (!existsSync(keyPath)) throw new CliError(\"not_found\", `No key file ${keyPath}: only the key that signed ${ref} (${appFingerprint(manifest.publisher)}) revokes its versions`);\n const key = readKey(keyPath, \"publisher\");\n if (key.key !== manifest.publisher) {\n throw refused(`${keyPath} is not the publisher key of ${ref} (${appFingerprint(manifest.publisher)}): a revocation by any other key is refused by every client`, { reason: \"signature-key\" });\n }\n\n const statement = (digests.length\n ? { ghostlyRevoke: 1, app: ref, digests }\n : { ghostlyRevoke: 1, app: ref, upTo: what.upTo }) as AppRevokeStatement;\n if (what.reason !== undefined) statement.reason = what.reason;\n const problem = checkAppRevokeStatement(statement);\n if (problem) throw refused(\"Not a revocation a client reads\", { reason: \"bad-revocation\", detail: problem });\n\n let list: SignedAppRevocation[] = [];\n if (existsSync(listPath)) {\n const held = readAppRevocations(new Uint8Array(readFileSync(listPath)));\n if (!held.ok) throw refused(`${listPath} is there and is not a valid ghostly-revoke.json: fix or move it first`, held);\n list = held.revocations;\n }\n const signed = await signAppRevocation(statement, key.signer);\n const same = list.some((r) => canonicalJson(r.statement) === canonicalJson(signed.statement));\n if (!same) {\n if (list.length >= APP_STATEMENT_LIMITS.revocations) throw refused(`${listPath} holds ${list.length} revocations already`, { reason: \"bad-revocation\", detail: `at most ${APP_STATEMENT_LIMITS.revocations}` });\n list = [...list, signed];\n const bytes = canonicalJsonBytes(list);\n const check = readAppRevocations(bytes);\n if (!check.ok) throw refused(\"The revocation list made is not valid\", check);\n writeWhole(listPath, bytes);\n }\n const covers = digests.length\n ? { digests, current: digests.includes(read.bundle.digest) }\n : { upTo: what.upTo, current: manifest.sequence <= what.upTo! };\n return { app: ref, ...covers, ...(what.reason !== undefined ? { reason: what.reason } : {}), added: !same, revocations: list.length, file: listPath };\n}\n\n// ---------- store indexes ----------\n\n/**\n * `store sign <index> --key <file> [--out <dir>]`: a store index written by hand (any JSON layout; `key` may be left\n * out and is then the signing key's) becomes `ghostly-store.json`, canonical, and `ghostly-store.sig`, in `--out` or\n * beside the index. It is read back as a client reads it (`expires` at most 90 days ahead), and refused when the index\n * already there, signed by the same key, has a higher `sequence`, or the same one with other content.\n */\nexport async function signStore(indexArg: string, keyArg: string, outArg?: string, now = Math.floor(Date.now() / 1000)): Promise<Record<string, unknown>> {\n const indexPath = resolve(indexArg);\n const keyPath = resolve(keyArg);\n const outDir = resolve(outArg ?? dirname(indexPath));\n let value: unknown;\n try { value = JSON.parse(readFileSync(indexPath, \"utf8\")); } catch (error) {\n if (error instanceof SyntaxError) throw new CliError(\"bad_request\", `${indexPath} is not JSON: ${error.message}`);\n throw error;\n }\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new CliError(\"bad_request\", `${indexPath} is not a JSON object`);\n const index = value as Record<string, unknown>;\n\n let key: LoadedKey;\n if (existsSync(keyPath)) key = readKey(keyPath, \"store\");\n else if (index.key !== undefined) throw new CliError(\"not_found\", `No key file ${keyPath}: the index names the store key ${typeof index.key === \"string\" ? appFingerprint(index.key) : JSON.stringify(index.key)}`);\n else key = makeKey(keyPath, \"store\");\n if (index.key !== undefined && index.key !== key.key) {\n throw refused(`${indexPath} names another store key than ${keyPath}'s (${appFingerprint(key.key)})`, { reason: \"store-key\" });\n }\n index.key = key.key;\n\n const checked = checkAppStoreIndex(index, { strict: true });\n if (!checked.ok) throw refused(`${indexPath} is not a valid store index`, checked);\n const { indexBytes, sigBytes } = await signAppStore(index as unknown as AppStoreIndex, key.signer);\n const read = readAppStore(indexBytes, sigBytes, now, key.key);\n if (!read.ok) throw refused(`${indexPath} would be refused by a client`, read);\n const store = read.store;\n\n // The index this one follows, when the folder holds one this key signed.\n const indexOut = join(outDir, STORE_INDEX_FILE), sigOut = join(outDir, STORE_SIG_FILE);\n let previous: number | null = null;\n if (existsSync(indexOut) && existsSync(sigOut)) {\n const held = readAppStore(new Uint8Array(readFileSync(indexOut)), new Uint8Array(readFileSync(sigOut)), now);\n if (held.ok && held.store.index.key === key.key) {\n previous = held.store.index.sequence;\n const decision = appStoreDecision(\n { key: key.key, sequence: held.store.index.sequence, digest: held.store.digest },\n { key: key.key, sequence: store.index.sequence, digest: store.digest },\n );\n if (decision === \"rollback\" || decision === \"equivocation\") {\n throw refused(`${indexOut} already holds sequence ${held.store.index.sequence} of this store: ${decision === \"rollback\" ? \"a lower one is refused by every client\" : \"the same one with other content is refused as equivocation\"}; raise sequence`, { reason: decision }, { held: held.store.index.sequence });\n }\n }\n }\n\n mkdirSync(outDir, { recursive: true });\n writeWhole(indexOut, indexBytes);\n writeWhole(sigOut, sigBytes);\n const notes = [\n ...(key.created ? [madeKeyNote(keyPath, \"store\", key.key)] : []),\n ...(store.expired ? [`expires (${new Date(store.index.expires * 1000).toISOString()}) is past: clients still install from it and say the store was not updated since then`] : []),\n ];\n return {\n key: key.key, fingerprint: appFingerprint(key.key), name: store.index.name, kind: store.index.kind,\n sequence: store.index.sequence, previous, expires: store.index.expires, expired: store.expired,\n apps: store.index.apps.length, removed: store.index.removed.length, revoked: store.index.revoked.length,\n digest: store.digest, index: indexOut, sig: sigOut, keyCreated: key.created,\n ...(notes.length ? { warning: notes.join(\"; \") } : {}),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"apps-BxV9i_Y4.js","names":[],"sources":["../../src/appTemplate.ts","../../src/apps.ts"],"sourcesContent":["/*\n * What `ghostly app init` writes (WISP 1200, Apps · Publishing): the smallest app that does something with a contact.\n * Three files, each whole in this module, so the CLI's one built file carries them: the manifest as a publisher writes\n * it, a page that says hello to the same app on the contact's side, and a README with the way from here to a store.\n */\n\nexport interface AppTemplate { name: string; title: string }\n\nconst html = (text: string) => text.replace(/[&<>\"]/g, (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\" })[c]!);\n\n/** `ghostly-app.json`: the manifest without `publisher`, `sequence` and `files`, which `app publish` writes. */\nexport function templateManifest({ name, title }: AppTemplate): Record<string, unknown> {\n return {\n name,\n version: \"0.1.0\",\n kind: \"mini-app\",\n title,\n tagline: \"Say hello to a contact\",\n description: \"Open it in a chat and your contact gets a card to join. Each side can then say hello to the other.\",\n entry: \"index.html\",\n permissions: [\"chat\"],\n view: \"chat\",\n runtime: { host: \">=1.2\", clients: [\"web\", \"desktop\"] },\n license: \"MIT\",\n };\n}\n\n/**\n * `index.html`: the whole app in one file, as the sandbox needs it (no network, so nothing is loaded from elsewhere).\n * Its script is plain JavaScript typed with JSDoc against `@ghostlytools/sdk/app`, so it runs as written and still type\n * checks (test/appInit.test.ts does). The title is the only thing put into it, as text. Its type imports are in single\n * quotes: they are text of the page, not imports of the CLI's own build (test/packageDeps.test.ts reads those).\n */\nexport function templatePage({ title }: AppTemplate): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${html(title)}</title>\n<style>\n :root { color-scheme: light dark; --bg: #ffffff; --fg: #16181d; --muted: #5b6270; --line: #d9dde3; --accent: #2f6fed; }\n :root[data-theme=\"dark\"] { --bg: #16181d; --fg: #f2f4f7; --muted: #a3abb8; --line: #343a45; --accent: #7aa5ff; }\n body { margin: 0; padding: 20px; background: var(--bg); color: var(--fg); font: 16px/1.4 system-ui, sans-serif; }\n h1 { margin: 0 0 4px; font-size: 20px; }\n #status { margin: 0 0 16px; color: var(--muted); }\n button { font: inherit; padding: 8px 16px; border: 1px solid var(--accent); border-radius: 8px; background: var(--accent); color: var(--bg); }\n button:disabled { opacity: 0.5; }\n ul { margin: 16px 0 0; padding: 0; list-style: none; }\n li { padding: 8px 0; border-top: 1px solid var(--line); }\n</style>\n</head>\n<body>\n<h1>${html(title)}</h1>\n<p id=\"status\" role=\"status\">Starting</p>\n<button id=\"hello\" type=\"button\" disabled>Say hello</button>\n<ul id=\"log\" aria-label=\"Messages\"></ul>\n<script type=\"module\">\n// The types of the API come from the SDK (npm install --save-dev @ghostlytools/sdk); nothing of it runs here.\n/** @typedef {import('@ghostlytools/sdk/app').MiniAppApi} MiniAppApi */\n/** @typedef {import('@ghostlytools/sdk/app').MiniAppJson} MiniAppJson */\n\n/** The longest text shown from a contact's frame. */\nconst MAX_TEXT = 200;\n\nconst status = /** @type {HTMLElement} */ (document.getElementById(\"status\"));\nconst button = /** @type {HTMLButtonElement} */ (document.getElementById(\"hello\"));\nconst log = /** @type {HTMLElement} */ (document.getElementById(\"log\"));\n\n/** @param {string} text */\nfunction show(text) {\n const line = document.createElement(\"li\");\n line.textContent = text;\n log.append(line);\n}\n\n/**\n * What the contact's app sent, when it is a hello. A frame comes from another device: read only what this app sends,\n * and never put it in the page as HTML.\n * @param {MiniAppJson} data\n * @returns {string | null}\n */\nfunction helloOf(data) {\n if (!data || typeof data !== \"object\" || Array.isArray(data) || data.type !== \"hello\") return null;\n return typeof data.text === \"string\" && data.text.length <= MAX_TEXT ? data.text : null;\n}\n\n/**\n * Why a send was refused, in words. The code is the rejection's message (MINI_APP_ERROR_CODES in the SDK); a later\n * client may add one.\n * @param {unknown} error\n */\nfunction refusal(error) {\n switch (error instanceof Error ? error.message : \"\") {\n case \"offline\": return \"Not sent: you are not both online\";\n case \"peer-closed\": case \"not-open\": return \"Not sent: your contact has not opened this app\";\n case \"not-allowed\": return \"Open this app in a chat to say hello\";\n default: return \"Not sent\";\n }\n}\n\n/** @param {MiniAppApi} ghostly */\nasync function start(ghostly) {\n // Listen before anything is awaited, so no frame is missed.\n ghostly.chat.on(\"message\", (data) => {\n const text = helloOf(data);\n if (text !== null) show(\"Your contact: \" + text);\n });\n ghostly.chat.on(\"peer\", (peer) => {\n status.textContent = peer.open ? \"Your contact has this app open\" : \"Waiting for your contact to open this app\";\n });\n\n const context = await ghostly.context();\n const dark = context.theme ? context.theme === \"dark\" : matchMedia(\"(prefers-color-scheme: dark)\").matches;\n document.documentElement.dataset.theme = dark ? \"dark\" : \"light\";\n status.textContent = !context.inChat ? \"Open this app in a chat to say hello\"\n : context.peer ? \"Your contact has this app open\" : \"Waiting for your contact to open this app\";\n\n button.disabled = false;\n button.addEventListener(\"click\", async () => {\n try {\n await ghostly.chat.send({ type: \"hello\", text: \"Hello!\" });\n show(\"You: Hello!\");\n } catch (error) {\n show(refusal(error));\n }\n });\n}\n\n/** Ghostly's runner gives the app \\`window.ghostly\\`; a browser tab has none. */\nconst ghostly = /** @type {{ ghostly?: MiniAppApi }} */ (window).ghostly;\nif (ghostly) void start(ghostly);\nelse status.textContent = \"This page is a Ghostly app: publish it and open it in Ghostly\";\n</script>\n</body>\n</html>\n`;\n}\n\n/** `README.md`: what the folder holds and the way from it to a store. */\nexport function templateReadme({ name, title }: AppTemplate): string {\n return `# ${title}\n\nA [Ghostly](https://github.com/MiguelMedeiros/ghostly) app, started with \\`ghostly app init\\`. Opened in a 1:1 chat, it\ngives your contact a card to join, and each side can say hello to the other.\n\nA Ghostly app ([WISP 1200](https://github.com/MiguelMedeiros/ghostly/blob/dev/docs/wisps/1200-marketplace.md)) is a\npage that runs in the client's sandbox. It has no network, and talks to Ghostly only through \\`window.ghostly\\`:\nframes of at most 32 KiB to the same app on the contact's side (\\`chat.send\\`, \\`chat.on\\`), storage per app and per\nchat, and the files of its own bundle. Apps run in Ghostly from release 1.2.\n\n| File | What it is |\n|---|---|\n| \\`ghostly-app.json\\` | The manifest, without \\`publisher\\`, \\`sequence\\` and \\`files\\`: \\`ghostly app publish\\` writes those |\n| \\`index.html\\` | The app, whole: styles and script inline |\n| \\`README.md\\` | This file |\n\n## Write it\n\n\\`index.html\\` is plain JavaScript, typed with JSDoc against \\`@ghostlytools/sdk/app\\`: \\`MiniAppApi\\` is\n\\`window.ghostly\\`, and the same module has the limits (\\`MINI_APP_LIMITS\\`) and the codes a refused call rejects with\n(\\`MINI_APP_ERROR_CODES\\`). For the types in your editor:\n\n\\`\\`\\`sh\nnpm install --save-dev @ghostlytools/sdk\n\\`\\`\\`\n\n- What a contact's app sends is untrusted: check its shape, bound its size, and show it as text, never as HTML.\n- \\`\"view\": \"chat\"\\` runs the app inside a chat with one contact. \\`\"full\"\\` is a full-screen app opened from the Apps page.\n- \\`permissions\\` lists what the person is asked for: \\`chat\\` (frames to the contact's app), \\`name\\` (the person's\n display name) and \\`internet\\`. Adding one later makes every install ask before it updates.\n\n## Build\n\nThere is nothing to build yet: the page is the app. Once it grows into several source files, or TypeScript, build it\ninto one self-contained \\`index.html\\` (scripts, styles and images inline) and put that file and \\`ghostly-app.json\\`\nalone in a folder of their own, which is the folder you publish.\n[ghostly-chess](https://github.com/MiguelMedeiros/ghostly-chess) does it with Vite and a small\n\\`scripts/stage.mjs\\`. \\`ghostly app publish\\` takes every file of the folder it is given but dot files, this README\nincluded, so do not run it on a repository with sources, \\`node_modules\\` or anything private in it.\n\n## Publish\n\n\\`\\`\\`sh\nghostly app publish . --key ~/ghostly-keys/${name}-publisher.key\nghostly app verify app.ghostlyapp\n\\`\\`\\`\n\n\\`publish\\` signs the folder into \\`app.ghostlyapp\\`. The first run makes the publisher key at the path you name:\n\n- Keep it **outside this folder** and its repository (\\`publish\\` refuses a key inside it), and never commit it.\n- Back it up. An app is updated only with the key that signed it; a lost key means a new app.\n\n\\`verify\\` checks the bundle as Ghostly will and prints its \\`ref\\` (\\`<publisher key>/${name}\\`), \\`sequence\\` and\n\\`digest\\`. For the next version, raise \\`version\\` in \\`ghostly-app.json\\` and publish again: the sequence goes up\nby one by itself.\n\n## Try it\n\nCommit \\`app.ghostlyapp\\` to a public GitHub repository, then in Ghostly open Apps, choose Add, and paste its URL\n(\\`https://raw.githubusercontent.com/<owner>/<repo>/HEAD/app.ghostlyapp\\`). Set \\`sources\\` in \\`ghostly-app.json\\`\nto that URL, in a list, so installs made from it find your updates. Open the app from a chat's + menu; your contact\ngets a card to install and open it.\n\n## List it in a store\n\nA store is a signed list of apps. The official one is a Git repository,\n[ghostly-store](https://github.com/MiguelMedeiros/ghostly-store), and apps enter it by pull request:\n\n1. Commit \\`app.ghostlyapp\\` to your public repository and note that commit.\n2. Add \\`apps/${name}.<first 16 characters of your publisher key>/listing.json\\` with the \\`ref\\`, \\`sequence\\`\n and \\`digest\\` that \\`ghostly app verify\\` printed. Its \\`urls\\` start with a jsDelivr URL pinned to the full commit\n (\\`https://cdn.jsdelivr.net/gh/<owner>/<repo>@<40-character commit>/app.ghostlyapp\\`).\n3. Open the pull request. Its check verifies the bundle; a maintainer reviews the app and the owner signs the index.\n\nThe store's [CONTRIBUTING](https://github.com/MiguelMedeiros/ghostly-store/blob/main/CONTRIBUTING.md) has the rules.\nAn app installed from a store updates only to the version that store lists, so each new version is a pull request too.\n\n## If something goes wrong\n\nIf a version is harmful, \\`ghostly app revoke . --key <your key> --up-to <sequence>\\` writes \\`ghostly-revoke.json\\`;\npublish it beside \\`app.ghostlyapp\\` and Ghostly stops those versions.\n`;\n}\n","import { createHash, createPrivateKey, generateKeyPairSync } from \"node:crypto\";\nimport { closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, writeSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport {\n APP_BUNDLE_LIMITS, APP_PREFIXES, appFingerprint, appRef, appStoreDecision, assembleAppBundle, canonicalJson, canonicalJsonBytes,\n APP_STATEMENT_LIMITS, checkAppManifest, checkAppRevokeStatement, checkAppStoreIndex, fromBase64Url, isAppUrl, readAppBundle,\n readAppRevocations, readAppStore, seedSigner, signAppObject, signAppRevocation, signAppStore, toBase64Url, toZ32, type AppBundle,\n type AppManifest, type AppRevokeStatement, type AppStoreIndex, type SignedAppRevocation, type Signer,\n} from \"@ghostly/core\";\nimport { templateManifest, templatePage, templateReadme } from \"./appTemplate\";\nimport { CliError } from \"./errors\";\n\n/*\n * The publisher tools of WISP 1200 (Apps · Publishing): `app init` writes a folder to start from, `app publish` bundles\n * and signs a folder, `app verify` checks a bundle as a client would, `store sign` signs a store index. They run here, on files: no profile, no daemon, no\n * network but the one read `app verify` makes of a URL.\n *\n * Keys. A publisher key and a store key are Ed25519 keys of their own, never a profile's (WISP 1200 · Publisher\n * identity). Each lives in a file the command names with `--key`: a PKCS #8 PEM block (what `openssl genpkey -algorithm\n * ed25519` writes, so secret scanners know it for a private key), after one line that says which kind it is. The file\n * is made owner-only (0600) when a command makes it, and refused when anyone else may read it. A key is never printed:\n * only its public key and fingerprint are.\n */\n\nexport const APP_SOURCE = \"ghostly-app.json\";\nexport const APP_BUNDLE_FILE = \"app.ghostlyapp\";\nexport const APP_REVOKE_FILE = \"ghostly-revoke.json\";\nexport const STORE_INDEX_FILE = \"ghostly-store.json\";\nexport const STORE_SIG_FILE = \"ghostly-store.sig\";\n\n/** How long `app verify` waits for a URL, and how many redirects it follows (each checked as the first). */\nconst FETCH_TIMEOUT_MS = 60_000;\nconst MAX_REDIRECTS = 5;\n\n// ---------- keys ----------\n\nexport type KeyKind = \"publisher\" | \"store\";\n\nconst KEY_HEADER: Record<KeyKind, string> = {\n publisher: \"Ghostly publisher key (WISP 1200). Secret: never share or commit it. Back it up: an app signed with it is updated only with it.\",\n store: \"Ghostly store key (WISP 1200). Secret: keep it offline, never share or commit it. Back it up: a store is its key.\",\n};\nconst PEM = /-----BEGIN PRIVATE KEY-----\\r?\\n([A-Za-z0-9+/=\\r\\n]+)-----END PRIVATE KEY-----/;\n\nexport interface LoadedKey { signer: Signer; key: string; created: boolean }\n\n/** Refuses a key file that is not a plain file of this user's, or that anyone else may read or write. */\nfunction checkKeyFile(path: string): void {\n const info = statSync(path);\n if (!info.isFile()) throw new CliError(\"bad_request\", `${path} is not a file: --key names a key file`);\n if (process.platform === \"win32\") return;\n if (process.getuid && info.uid !== process.getuid()) throw new CliError(\"refused\", `${path} is not this user's: a key file must be yours alone`);\n if ((info.mode & 0o077) !== 0) {\n throw new CliError(\"refused\", `${path} may be read by others (mode ${(info.mode & 0o777).toString(8)}): run chmod 600 ${path}, and think whether the key was seen`);\n }\n}\n\n/** Reads a key file: the PEM block, an Ed25519 key, and the kind its first line names (when it names one). */\nfunction readKey(path: string, kind: KeyKind): LoadedKey {\n checkKeyFile(path);\n const text = readFileSync(path, \"utf8\");\n const other: KeyKind = kind === \"publisher\" ? \"store\" : \"publisher\";\n if (text.startsWith(`Ghostly ${other} key`)) {\n throw new CliError(\"refused\", `${path} is a ${other} key: ${kind === \"publisher\" ? \"a store key never signs an app\" : \"a publisher key never signs a store index\"} (WISP 1200 · Stores)`);\n }\n const block = PEM.exec(text);\n let seed: Uint8Array | undefined;\n if (block) {\n try {\n const key = createPrivateKey(block[0]);\n const d = key.asymmetricKeyType === \"ed25519\" ? key.export({ format: \"jwk\" }).d : undefined;\n if (d) seed = fromBase64Url(d);\n } catch { /* said below, without the file's text */ }\n }\n if (!seed || seed.length !== 32) throw new CliError(\"bad_request\", `${path} holds no Ed25519 private key (a PEM \"PRIVATE KEY\" block, as openssl genpkey -algorithm ed25519 writes)`);\n const signer = seedSigner(seed);\n return { signer, key: toZ32(signer.publicKey), created: false };\n}\n\n/** Makes a key file: owner-only from the start, never over a file that is there. */\nfunction makeKey(path: string, kind: KeyKind): LoadedKey {\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n const { privateKey } = generateKeyPairSync(\"ed25519\");\n const pem = privateKey.export({ type: \"pkcs8\", format: \"pem\" }) as string;\n const fd = openSync(path, \"wx\", 0o600);\n try { writeSync(fd, `${KEY_HEADER[kind]}\\n${pem}`); } finally { closeSync(fd); }\n return { ...readKey(path, kind), created: true };\n}\n\n/** What the person is told on stderr when a key was made: where, which, and to back it up. Never the key. */\nfunction madeKeyNote(path: string, kind: KeyKind, key: string): string {\n return kind === \"publisher\"\n ? `new publisher key written to ${path}; back it up (${appFingerprint(key)}: an app is updated only with the key that signed it, and phase 1 has no recovery)`\n : `new store key written to ${path}; back it up and keep it offline (${appFingerprint(key)}: a store is its key)`;\n}\n\n// ---------- reading bundles ----------\n\nconst refused = (what: string, reading: { reason: string; detail?: string }, extra: Record<string, unknown> = {}) =>\n new CliError(\"refused\", `${what} (${reading.reason}${reading.detail ? `: ${reading.detail}` : \"\"})`, { reason: reading.reason, ...(reading.detail ? { detail: reading.detail } : {}), ...extra });\n\n/** A bundle file's bytes: its size is checked before it is read, so a huge file is refused unread. */\nfunction readBundleFile(path: string): Uint8Array {\n const info = statSync(path);\n if (info.isDirectory()) throw new CliError(\"bad_request\", `${path} is a folder: name a .ghostlyapp file`);\n if (info.size > APP_BUNDLE_LIMITS.bundleBytes) throw refused(`${path} is not a valid bundle`, { reason: \"too-large\", detail: `${info.size} bytes` });\n return new Uint8Array(readFileSync(path));\n}\n\n/**\n * A bundle at an https URL, as a client fetches one: `https:` only, no user or password, jsDelivr only at a full\n * commit (WISP 1200 · Manifest, \"Every URL\"); every redirect is held to the same rule, and the download stops past\n * 16 MiB.\n */\nasync function fetchBundle(url: string): Promise<Uint8Array> {\n let at = url;\n for (let hop = 0; ; hop++) {\n if (!isAppUrl(at)) {\n throw new CliError(hop ? \"refused\" : \"usage\", hop\n ? `${url} redirects to ${at}, which a client does not read (https only, no user or password, jsDelivr only at a full commit)`\n : `Not a URL a client reads: https only, at most ${APP_BUNDLE_LIMITS.url} characters, no user or password, and jsDelivr only as https://cdn.jsdelivr.net/gh/<owner>/<repo>@<40-hex commit>/<path>`);\n }\n let response: Response;\n try {\n response = await fetch(at, { redirect: \"manual\", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });\n } catch (error) {\n if (error instanceof Error && error.name === \"TimeoutError\") throw new CliError(\"timeout\", `${at} did not answer in ${FETCH_TIMEOUT_MS / 1000} s`);\n throw new CliError(\"unavailable\", `Could not read ${at}: ${error instanceof Error ? error.message : String(error)}`);\n }\n if (response.status >= 300 && response.status < 400 && response.headers.get(\"location\")) {\n await response.body?.cancel();\n if (hop >= MAX_REDIRECTS) throw new CliError(\"refused\", `${url} redirects more than ${MAX_REDIRECTS} times`);\n at = new URL(response.headers.get(\"location\")!, at).href;\n continue;\n }\n if (response.status === 404 || response.status === 410) { await response.body?.cancel(); throw new CliError(\"not_found\", `${at} answered ${response.status}`); }\n if (!response.ok || !response.body) { await response.body?.cancel(); throw new CliError(\"unavailable\", `${at} answered ${response.status}`); }\n const limit = APP_BUNDLE_LIMITS.bundleBytes;\n const tooLarge = (bytes: number | string) => refused(`${url} is not a valid bundle`, { reason: \"too-large\", detail: `${bytes} bytes` });\n const length = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(length) && length > limit) { await response.body.cancel(); throw tooLarge(length); }\n const chunks: Uint8Array[] = [];\n let total = 0;\n const reader = response.body.getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.length;\n if (total > limit) { await reader.cancel(); throw tooLarge(`more than ${limit}`); }\n chunks.push(value);\n }\n } catch (error) {\n if (error instanceof CliError) throw error;\n if (error instanceof Error && error.name === \"TimeoutError\") throw new CliError(\"timeout\", `${at} did not finish in ${FETCH_TIMEOUT_MS / 1000} s`);\n throw new CliError(\"unavailable\", `Could not read ${at}: ${error instanceof Error ? error.message : String(error)}`);\n }\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) { out.set(chunk, offset); offset += chunk.length; }\n return out;\n }\n}\n\n/** What a checked bundle is, as `app verify` and `app publish` print it. */\nfunction described(bundle: AppBundle, bytes: Uint8Array) {\n const m = bundle.manifest;\n return {\n ref: appRef(m.publisher, m.name), publisher: m.publisher, fingerprint: appFingerprint(m.publisher), name: m.name,\n title: m.title, version: m.version, sequence: m.sequence, digest: bundle.digest, bytes: bytes.length,\n sha256: toBase64Url(createHash(\"sha256\").update(bytes).digest()),\n };\n}\n\n/** `app verify <bundle|url>`: every check a client makes before it stores a bundle (WISP 1200 · Reading a bundle). */\nexport async function verifyApp(source: string): Promise<Record<string, unknown>> {\n const { bundle, bytes } = await checkedBundle(source);\n return { valid: true, ...described(bundle, bytes), manifest: bundle.manifest };\n}\n\n/** A bundle from a file or an https URL, with every check of `app verify`; `url` when it came from one. */\nexport async function checkedBundle(source: string): Promise<{ bundle: AppBundle; bytes: Uint8Array; url?: string }> {\n const isUrl = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(source);\n const bytes = isUrl ? await fetchBundle(source) : readBundleFile(resolve(source));\n const read = readAppBundle(bytes);\n if (!read.ok) throw refused(`${source} is not a valid bundle`, read);\n return { bundle: read.bundle, bytes, ...(isUrl && { url: source }) };\n}\n\n// ---------- starting an app ----------\n\n/** A folder's name as an app's: lower case, anything else a hyphen (`My App` is `my-app`). */\nconst nameFrom = (folder: string) => folder.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n/** An app's name as a title: `my-app` is `My app`. */\nconst titleFrom = (name: string) => (name[0]!.toUpperCase() + name.slice(1)).replace(/-/g, \" \");\n\n/**\n * `app init <dir> [--name <name>] [--title <title>] [--force]`: a small working app in `<dir>` (made when missing),\n * which `app publish <dir>` signs as it is: `ghostly-app.json`, an `index.html` that says hello to the contact, and a\n * README with the way to a store (./appTemplate.ts). The name is `--name`, else the folder's; the title `--title`, else\n * the name's. Nothing is written when one of the three files is there, unless `--force`, which replaces those three only.\n */\nexport async function initApp(dirArg: string, what: { name?: string; title?: string; force?: boolean } = {}): Promise<Record<string, unknown>> {\n const usage = \"ghostly app init <dir> [--name <name>] [--title <title>] [--force]\";\n const dir = resolve(dirArg);\n const name = what.name ?? nameFrom(basename(dir));\n const title = what.title ?? (name ? titleFrom(name) : \"\");\n const page = templatePage({ name, title });\n // Checked as publish will check it, under a key of zeros and the page's own entry: what is written always publishes.\n const pageBytes = new TextEncoder().encode(page);\n const checked = checkAppManifest({\n ...templateManifest({ name, title }), ghostlyApp: 1, publisher: toZ32(new Uint8Array(32)), sequence: 1,\n files: [{ path: \"index.html\", size: pageBytes.length, sha256: toBase64Url(createHash(\"sha256\").update(pageBytes).digest()) }],\n }, { strict: true });\n if (!checked.ok) {\n if (checked.detail === \"name is not valid\") {\n throw new CliError(\"usage\", what.name === undefined\n ? `The folder's name gives no app name (${JSON.stringify(basename(dir))}): give --name, a lowercase letter then up to 31 lowercase letters, digits or hyphens: ${usage}`\n : `--name is a lowercase letter then up to 31 lowercase letters, digits or hyphens, not ${JSON.stringify(what.name)}`);\n }\n if (checked.detail === \"title is not valid\") throw new CliError(\"usage\", `--title is one line of 1 to ${APP_BUNDLE_LIMITS.title} characters, not ${JSON.stringify(title)}`);\n throw refused(\"The template makes no valid manifest\", checked);\n }\n\n if (existsSync(dir) && !statSync(dir).isDirectory()) throw new CliError(\"bad_request\", `${dir} is a file: name a folder for the app`);\n const files: [string, string][] = [\n [APP_SOURCE, `${JSON.stringify(templateManifest({ name, title }), null, 2)}\\n`],\n [\"index.html\", page],\n [\"README.md\", templateReadme({ name, title })],\n ];\n const held = files.map(([path]) => path).filter((path) => existsSync(join(dir, path)));\n if (held.length && !what.force) {\n throw refused(`${dir} already holds ${held.join(\", \")}: nothing was written (--force replaces ${held.length > 1 ? \"them\" : \"it\"})`, { reason: \"exists\" }, { files: held });\n }\n mkdirSync(dir, { recursive: true });\n for (const [path, text] of files) writeWhole(join(dir, path), new TextEncoder().encode(text));\n return {\n dir, name, title, files: files.map(([path]) => path), ...(held.length ? { replaced: held } : {}),\n next: `ghostly app publish ${dirArg} --key <a key file outside ${dirArg}>`,\n };\n}\n\n// ---------- publishing ----------\n\n/** Every file of the folder, as the bundle names it; dot files and folders are left out and listed. */\nfunction collectFiles(dir: string, leaveOut: Set<string>): { files: { path: string; full: string; size: number }[]; skipped: string[] } {\n const files: { path: string; full: string; size: number }[] = [];\n const skipped: string[] = [];\n let bytes = 0;\n const walk = (folder: string) => {\n for (const name of readdirSync(folder).sort()) {\n const full = join(folder, name);\n const path = relative(dir, full).split(sep).join(\"/\");\n if (leaveOut.has(full)) continue;\n if (name.startsWith(\".\")) { skipped.push(path); continue; }\n const info = lstatSync(full);\n if (info.isSymbolicLink()) throw new CliError(\"bad_request\", `${path} is a link: a bundle holds the folder's own files (copy it in, or remove it)`);\n if (info.isDirectory()) { walk(full); continue; }\n if (!info.isFile()) throw new CliError(\"bad_request\", `${path} is not a file`);\n files.push({ path, full, size: info.size });\n bytes += info.size;\n if (files.length > APP_BUNDLE_LIMITS.files) throw refused(`${dir} holds more files than a bundle may`, { reason: \"too-many-files\", detail: `more than ${APP_BUNDLE_LIMITS.files}` });\n if (bytes > APP_BUNDLE_LIMITS.bundleBytes) throw refused(`${dir} holds more than a bundle may`, { reason: \"too-large\", detail: `more than ${APP_BUNDLE_LIMITS.bundleBytes} bytes of files` });\n }\n };\n walk(dir);\n // The manifest lists every path in code-unit order, across folders: `lib-extra.js` before `lib/x.js`, which the walk\n // reaches first (`-` and `.` sort before `/`).\n files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n return { files, skipped };\n}\n\n/**\n * A private key in a file: a PEM private key block with its body (PKCS #8, OpenSSH, RSA, EC, encrypted), or a Ghostly\n * key file's first line. A library that only names the block's header in its code does not match.\n */\nconst PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----\\r?\\n[A-Za-z0-9+/=\\r\\n:,-]{40,}-----END [A-Z0-9 ]*PRIVATE KEY-----|^Ghostly (publisher|store) key \\(WISP 1200\\)/;\n\n/** Whether `path` is `dir` or anything under it. */\nconst isInside = (dir: string, path: string) => { const rel = relative(dir, path); return !rel.startsWith(\"..\") && !isAbsolute(rel); };\n\n/** Fields of `ghostly-app.json` the command writes itself. */\nconst WRITTEN = [\"publisher\", \"files\"] as const;\n\nfunction readSource(path: string): Record<string, unknown> {\n if (!existsSync(path)) throw new CliError(\"not_found\", `No ${APP_SOURCE} in ${dirname(path)}: it holds the manifest's fields (name, version, title, tagline, entry, permissions, runtime, license, ...) but publisher, sequence and files, which publish writes`);\n let value: unknown;\n try { value = JSON.parse(readFileSync(path, \"utf8\")); } catch (error) { throw new CliError(\"bad_request\", `${path} is not JSON: ${error instanceof Error ? error.message : String(error)}`); }\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new CliError(\"bad_request\", `${path} is not a JSON object`);\n const draft = value as Record<string, unknown>;\n const written = WRITTEN.filter((k) => k in draft);\n if (written.length) throw new CliError(\"bad_request\", `${path} has ${written.join(\" and \")}: publish writes ${written.length > 1 ? \"them\" : \"it\"} from the key and the folder's files`);\n if (draft.ghostlyApp !== undefined && draft.ghostlyApp !== 1) throw refused(`${path} is not a manifest this release makes`, { reason: \"unsupported-format\", detail: \"ghostlyApp is not 1\" });\n if (draft.sequence !== undefined && !(Number.isSafeInteger(draft.sequence) && (draft.sequence as number) >= 1)) {\n throw new CliError(\"bad_request\", `${path}: sequence is a whole number from 1 (or leave it out: publish raises it by itself)`);\n }\n return draft;\n}\n\n/** Writes a file whole or not at all: a reader never sees half a bundle (and a dot file left by a crash is never bundled). */\nfunction writeWhole(path: string, bytes: Uint8Array): void {\n const tmp = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);\n writeFileSync(tmp, bytes);\n renameSync(tmp, path);\n}\n\n/**\n * `app publish <dir> --key <file> [--out <file>]`: `<dir>/ghostly-app.json` (the manifest without `publisher`,\n * `sequence` and `files`) and every other file in `<dir>` become one signed bundle, by default `<dir>/app.ghostlyapp`.\n * The sequence is `--sequence` (higher than the bundle already at `--out`'s), else one more than that bundle's (or\n * the source's `sequence`, if higher, else 1). The\n * key file is made when it is missing and no earlier bundle is there to update.\n */\nexport async function publishApp(dirArg: string, keyArg: string, outArg?: string, sequenceArg?: number): Promise<Record<string, unknown>> {\n if (sequenceArg !== undefined && !(Number.isSafeInteger(sequenceArg) && sequenceArg >= 1)) throw new CliError(\"usage\", `--sequence takes a whole number from 1, not ${sequenceArg}`);\n const dir = resolve(dirArg);\n if (!statSync(dir).isDirectory()) throw new CliError(\"bad_request\", `${dir} is not a folder`);\n const keyPath = resolve(keyArg);\n // Every file of the folder is published, and the folder is often the repository the bundle is committed to.\n if (isInside(dir, keyPath)) {\n throw refused(`${keyPath} is inside ${dir}, which is published: keep the publisher key outside the app's folder (and its repository)`, { reason: \"private-key\" });\n }\n const out = resolve(outArg ?? join(dir, APP_BUNDLE_FILE));\n const sourcePath = join(dir, APP_SOURCE);\n const draft = readSource(sourcePath);\n\n // The version this one follows: the bundle already at the output, which only its own key may follow.\n let previous: AppManifest | null = null;\n if (existsSync(out)) {\n const read = readAppBundle(readBundleFile(out));\n if (!read.ok) throw refused(`${out} is there and is not a valid bundle: move it away, or name another --out`, read);\n previous = read.bundle.manifest;\n }\n\n let key: LoadedKey;\n if (existsSync(keyPath)) key = readKey(keyPath, \"publisher\");\n else if (previous) {\n throw new CliError(\"not_found\", `No key file ${keyPath}: ${out} is signed by ${appFingerprint(previous.publisher)}, and only that key publishes its next version`);\n } else key = makeKey(keyPath, \"publisher\");\n\n if (previous && previous.publisher !== key.key) {\n throw refused(`${out} is ${appRef(previous.publisher, previous.name)}, signed by another key (${appFingerprint(previous.publisher)}): a version of an app is signed by its own key`, { reason: \"other-app\" }, { publisher: previous.publisher });\n }\n if (previous && previous.name !== draft.name) {\n throw refused(`${out} is the app ${JSON.stringify(previous.name)}, not ${JSON.stringify(draft.name)}: name another --out for another app`, { reason: \"other-app\" });\n }\n\n if (sequenceArg !== undefined && previous && sequenceArg <= previous.sequence) {\n throw refused(`--sequence ${sequenceArg} is not higher than the ${previous.sequence} of ${out}: every client refuses a lower one, and the same one with other content is equivocation`, { reason: sequenceArg < previous.sequence ? \"rollback\" : \"equivocation\" }, { previous: previous.sequence });\n }\n const sequence = sequenceArg ?? Math.max(previous ? previous.sequence + 1 : 1, (draft.sequence as number | undefined) ?? 1);\n const { files, skipped } = collectFiles(dir, new Set([sourcePath, out, join(dir, APP_REVOKE_FILE)]));\n const contents = files.map((f) => ({ path: f.path, bytes: new Uint8Array(readFileSync(f.full)) }));\n // A bundle is public: a private key found in the folder (an old key, a store key, any PEM) stops the publish.\n const keyFile = contents.find((f) => PRIVATE_KEY.test(Buffer.from(f.bytes).toString(\"latin1\")));\n if (keyFile) throw refused(`${keyFile.path} in ${dir} holds a private key, and a bundle is public: move it out of the app's folder`, { reason: \"private-key\", detail: keyFile.path });\n const manifest = {\n ...draft,\n ghostlyApp: 1,\n publisher: key.key,\n sequence,\n files: contents.map((f) => ({ path: f.path, size: f.bytes.length, sha256: toBase64Url(createHash(\"sha256\").update(f.bytes).digest()) })),\n };\n const checked = checkAppManifest(JSON.parse(JSON.stringify(manifest)), { strict: true });\n if (!checked.ok) throw refused(`${sourcePath} and the folder's files make no valid manifest`, checked);\n\n const { bytes: manifestBytes, signature } = await signAppObject(APP_PREFIXES.app, checked.manifest, key.signer);\n const bundle = assembleAppBundle(manifestBytes, canonicalJsonBytes(signature), contents.map((f) => f.bytes));\n // Read back as a client would (the icon's header, the bundle's size): what is written is never a bundle it refuses.\n const read = readAppBundle(bundle);\n if (!read.ok) throw refused(\"The bundle made is not valid\", read);\n mkdirSync(dirname(out), { recursive: true });\n writeWhole(out, bundle);\n\n return {\n ...described(read.bundle, bundle),\n previous: previous?.sequence ?? null,\n out,\n files: contents.map((f) => f.path),\n ...(skipped.length ? { skipped } : {}),\n keyCreated: key.created,\n ...(key.created ? { warning: madeKeyNote(keyPath, \"publisher\", key.key) } : {}),\n };\n}\n\n// ---------- revocations ----------\n\n/**\n * `app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>] [--bundle <file>]`: a\n * `ghostly-revoke/1` statement (WISP 1200 · Publisher keys, Revocation) for the app whose bundle is `<dir>/app.ghostlyapp`\n * (or `--bundle`), signed by that app's publisher key and added to `<dir>/ghostly-revoke.json`, which stays canonical.\n * The key is never made here: only the key that signed the app may revoke its versions.\n */\nexport async function revokeApp(dirArg: string, keyArg: string, what: { digests?: string[]; upTo?: number; reason?: string; bundle?: string }): Promise<Record<string, unknown>> {\n const dir = resolve(dirArg);\n const bundlePath = resolve(what.bundle ?? join(dir, APP_BUNDLE_FILE));\n const listPath = join(dir, APP_REVOKE_FILE);\n const usage = \"ghostly app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>]\";\n const digests = what.digests ?? [];\n if ((digests.length > 0) === (what.upTo !== undefined)) throw new CliError(\"usage\", `Give --digest (again for each) or --up-to, one of the two: ${usage}`);\n if (what.upTo !== undefined && !(Number.isSafeInteger(what.upTo) && what.upTo >= 1)) throw new CliError(\"usage\", `--up-to takes a sequence, a whole number from 1, not ${what.upTo}`);\n\n if (!existsSync(bundlePath)) throw new CliError(\"not_found\", `No bundle ${bundlePath}: a revocation names the app of the bundle in <dir> (or --bundle)`);\n const read = readAppBundle(readBundleFile(bundlePath));\n if (!read.ok) throw refused(`${bundlePath} is not a valid bundle`, read);\n const manifest = read.bundle.manifest;\n const ref = appRef(manifest.publisher, manifest.name);\n\n const keyPath = resolve(keyArg);\n if (!existsSync(keyPath)) throw new CliError(\"not_found\", `No key file ${keyPath}: only the key that signed ${ref} (${appFingerprint(manifest.publisher)}) revokes its versions`);\n const key = readKey(keyPath, \"publisher\");\n if (key.key !== manifest.publisher) {\n throw refused(`${keyPath} is not the publisher key of ${ref} (${appFingerprint(manifest.publisher)}): a revocation by any other key is refused by every client`, { reason: \"signature-key\" });\n }\n\n const statement = (digests.length\n ? { ghostlyRevoke: 1, app: ref, digests }\n : { ghostlyRevoke: 1, app: ref, upTo: what.upTo }) as AppRevokeStatement;\n if (what.reason !== undefined) statement.reason = what.reason;\n const problem = checkAppRevokeStatement(statement);\n if (problem) throw refused(\"Not a revocation a client reads\", { reason: \"bad-revocation\", detail: problem });\n\n let list: SignedAppRevocation[] = [];\n if (existsSync(listPath)) {\n const held = readAppRevocations(new Uint8Array(readFileSync(listPath)));\n if (!held.ok) throw refused(`${listPath} is there and is not a valid ghostly-revoke.json: fix or move it first`, held);\n list = held.revocations;\n }\n const signed = await signAppRevocation(statement, key.signer);\n const same = list.some((r) => canonicalJson(r.statement) === canonicalJson(signed.statement));\n if (!same) {\n if (list.length >= APP_STATEMENT_LIMITS.revocations) throw refused(`${listPath} holds ${list.length} revocations already`, { reason: \"bad-revocation\", detail: `at most ${APP_STATEMENT_LIMITS.revocations}` });\n list = [...list, signed];\n const bytes = canonicalJsonBytes(list);\n const check = readAppRevocations(bytes);\n if (!check.ok) throw refused(\"The revocation list made is not valid\", check);\n writeWhole(listPath, bytes);\n }\n const covers = digests.length\n ? { digests, current: digests.includes(read.bundle.digest) }\n : { upTo: what.upTo, current: manifest.sequence <= what.upTo! };\n return { app: ref, ...covers, ...(what.reason !== undefined ? { reason: what.reason } : {}), added: !same, revocations: list.length, file: listPath };\n}\n\n// ---------- store indexes ----------\n\n/**\n * `store sign <index> --key <file> [--out <dir>]`: a store index written by hand (any JSON layout; `key` may be left\n * out and is then the signing key's) becomes `ghostly-store.json`, canonical, and `ghostly-store.sig`, in `--out` or\n * beside the index. It is read back as a client reads it (`expires` at most 90 days ahead), and refused when the index\n * already there, signed by the same key, has a higher `sequence`, or the same one with other content.\n */\nexport async function signStore(indexArg: string, keyArg: string, outArg?: string, now = Math.floor(Date.now() / 1000)): Promise<Record<string, unknown>> {\n const indexPath = resolve(indexArg);\n const keyPath = resolve(keyArg);\n const outDir = resolve(outArg ?? dirname(indexPath));\n let value: unknown;\n try { value = JSON.parse(readFileSync(indexPath, \"utf8\")); } catch (error) {\n if (error instanceof SyntaxError) throw new CliError(\"bad_request\", `${indexPath} is not JSON: ${error.message}`);\n throw error;\n }\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new CliError(\"bad_request\", `${indexPath} is not a JSON object`);\n const index = value as Record<string, unknown>;\n\n let key: LoadedKey;\n if (existsSync(keyPath)) key = readKey(keyPath, \"store\");\n else if (index.key !== undefined) throw new CliError(\"not_found\", `No key file ${keyPath}: the index names the store key ${typeof index.key === \"string\" ? appFingerprint(index.key) : JSON.stringify(index.key)}`);\n else key = makeKey(keyPath, \"store\");\n if (index.key !== undefined && index.key !== key.key) {\n throw refused(`${indexPath} names another store key than ${keyPath}'s (${appFingerprint(key.key)})`, { reason: \"store-key\" });\n }\n index.key = key.key;\n\n const checked = checkAppStoreIndex(index, { strict: true });\n if (!checked.ok) throw refused(`${indexPath} is not a valid store index`, checked);\n const { indexBytes, sigBytes } = await signAppStore(index as unknown as AppStoreIndex, key.signer);\n const read = readAppStore(indexBytes, sigBytes, now, key.key);\n if (!read.ok) throw refused(`${indexPath} would be refused by a client`, read);\n const store = read.store;\n\n // The index this one follows, when the folder holds one this key signed.\n const indexOut = join(outDir, STORE_INDEX_FILE), sigOut = join(outDir, STORE_SIG_FILE);\n let previous: number | null = null;\n if (existsSync(indexOut) && existsSync(sigOut)) {\n const held = readAppStore(new Uint8Array(readFileSync(indexOut)), new Uint8Array(readFileSync(sigOut)), now);\n if (held.ok && held.store.index.key === key.key) {\n previous = held.store.index.sequence;\n const decision = appStoreDecision(\n { key: key.key, sequence: held.store.index.sequence, digest: held.store.digest },\n { key: key.key, sequence: store.index.sequence, digest: store.digest },\n );\n if (decision === \"rollback\" || decision === \"equivocation\") {\n throw refused(`${indexOut} already holds sequence ${held.store.index.sequence} of this store: ${decision === \"rollback\" ? \"a lower one is refused by every client\" : \"the same one with other content is refused as equivocation\"}; raise sequence`, { reason: decision }, { held: held.store.index.sequence });\n }\n }\n }\n\n mkdirSync(outDir, { recursive: true });\n writeWhole(indexOut, indexBytes);\n writeWhole(sigOut, sigBytes);\n const notes = [\n ...(key.created ? [madeKeyNote(keyPath, \"store\", key.key)] : []),\n ...(store.expired ? [`expires (${new Date(store.index.expires * 1000).toISOString()}) is past: clients still install from it and say the store was not updated since then`] : []),\n ];\n return {\n key: key.key, fingerprint: appFingerprint(key.key), name: store.index.name, kind: store.index.kind,\n sequence: store.index.sequence, previous, expires: store.index.expires, expired: store.expired,\n apps: store.index.apps.length, removed: store.index.removed.length, revoked: store.index.revoked.length,\n digest: store.digest, index: indexOut, sig: sigOut, keyCreated: key.created,\n ...(notes.length ? { warning: notes.join(\"; \") } : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { c as publishApp, d as verifyApp, l as revokeApp, s as initApp, u as signStore } from "./apps-
|
|
1
|
+
import { c as publishApp, d as verifyApp, l as revokeApp, s as initApp, u as signStore } from "./apps-BxV9i_Y4.js";
|
|
2
2
|
export { initApp, publishApp, revokeApp, signStore, verifyApp };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { gn as validatePaymentTarget, mn as assertWholeSats } from "./src-
|
|
1
|
+
import { gn as validatePaymentTarget, mn as assertWholeSats } from "./src-DDzSERyc.js";
|
|
2
2
|
import { ArkAddress, EsploraProvider, IndexedDBContractRepository, IndexedDBWalletRepository, MnemonicIdentity, RestArkProvider, RestIndexerProvider, Transaction, Wallet, assertAllowedSighashTypes, assertSubmittedArkTxid, canRecoverOnchain, isRecoverable, matchServerCheckpoints, verifyTapscriptSignatures } from "@arkade-os/sdk";
|
|
3
3
|
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
4
4
|
//#region ../browser/src/engine/paymentAdapters/arkade.ts
|
|
@@ -359,4 +359,4 @@ var ArkadeAdapter = class ArkadeAdapter {
|
|
|
359
359
|
//#endregion
|
|
360
360
|
export { ARK_NETWORKS, ArkadeAdapter };
|
|
361
361
|
|
|
362
|
-
//# sourceMappingURL=arkade-
|
|
362
|
+
//# sourceMappingURL=arkade-C_qDmHnm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arkade-D4BVJFEQ.js","names":[],"sources":["../../../browser/src/engine/paymentAdapters/arkade.ts"],"sourcesContent":["import {\n Wallet, MnemonicIdentity, RestArkProvider, RestIndexerProvider, EsploraProvider,\n IndexedDBWalletRepository, IndexedDBContractRepository, ArkAddress, Transaction,\n assertSubmittedArkTxid, matchServerCheckpoints, assertAllowedSighashTypes, verifyTapscriptSignatures,\n isRecoverable, canRecoverOnchain, type StorageConfig,\n} from \"@arkade-os/sdk\";\nimport { tapLeafHash } from \"@scure/btc-signer/payment.js\";\nimport { assertWholeSats, validatePaymentTarget, type PaymentAdapter, type PaymentReview, type PaymentTarget } from \"@ghostly/core\";\n\nexport interface ArkConfig { network: \"bitcoin\" | \"regtest\" | \"signet\" | \"mutinynet\"; provider: string; explorer: string; serverKey: string; walletId: string }\nexport interface ArkPrepared { signedTx: string; checkpoints: string[]; txid: string; address: string; amount: number; fee: number; finalCheckpoints?: string[] }\nconst unbase64 = (s: string) => Uint8Array.from(atob(s),c=>c.charCodeAt(0));\nconst base64 = (bytes: Uint8Array) => btoa(Array.from(bytes,b=>String.fromCharCode(b)).join(\"\"));\nconst hex = (bytes: Uint8Array) => Array.from(bytes,b=>b.toString(16).padStart(2,\"0\")).join(\"\");\nclass PreparedOnly extends Error {}\nexport const ARK_NETWORKS: ArkConfig[\"network\"][] = [\"bitcoin\", \"mutinynet\", \"signet\", \"regtest\"];\n/** Payments go through review and approval; only the SDK's own settlement (renewal, onboarding) reaches the provider directly. */\nclass PreviewProvider extends RestArkProvider {\n authorizedFinalize?:{txid:string;run:(checkpoints:string[])=>Promise<void>};\n captured?: { signedTx:string; checkpoints:string[] };\n take() { return this.captured; }\n override async submitTx(signedTx:string,checkpoints:string[]):Promise<never> { this.captured={signedTx,checkpoints};throw new PreparedOnly(); }\n override async finalizeTx(txid:string,checkpoints:string[]):Promise<void> {\n if(this.authorizedFinalize?.txid!==txid)throw new Error(\"Preview cannot finalize an unapproved payment\");\n await this.authorizedFinalize.run(checkpoints);\n }\n}\nexport class ArkadeAdapter implements PaymentAdapter<ArkPrepared> {\n readonly method = \"arkade\" as const;\n private pending:Promise<unknown>=Promise.resolve();\n private serial<T>(run:()=>Promise<T>):Promise<T>{const next=this.pending.then(run,run);this.pending=next.catch(()=>{});return next;}\n prepare(target:PaymentTarget,amount:number,feeCap:number){return this.serial(()=>this.prepareOperation(target,amount,feeCap));}\n execute(review:PaymentReview,prepared:ArkPrepared,persist?:()=>Promise<void>){return this.serial(()=>this.executeOperation(review,prepared,persist));}\n reconcile(review:PaymentReview,prepared:ArkPrepared,persist?:()=>Promise<void>){return this.serial(()=>this.reconcileOperation(review,prepared,persist));}\n private constructor(readonly config: ArkConfig, private identity: MnemonicIdentity, private wallet: Wallet, private preview: PreviewProvider, private provider: RestArkProvider, private indexer: RestIndexerProvider,private repository:NonNullable<StorageConfig[\"walletRepository\"]>) {}\n static async connect(config: ArkConfig, mnemonic: string, storage?: StorageConfig): Promise<ArkadeAdapter> {\n if (!ARK_NETWORKS.includes(config.network)) throw new Error(\"Unsupported Ark network\");\n validatePaymentTarget({method:\"arkade\",network:config.network,provider:config.provider,asset:\"BTC\",unit:\"sat\",address:\"configuration\",expiresAt:Date.now()+60000});\n validatePaymentTarget({method:\"arkade\",network:config.network,provider:config.explorer,asset:\"BTC\",unit:\"sat\",address:\"configuration\",expiresAt:Date.now()+60000});\n const provider=new RestArkProvider(config.provider);\n const info=await provider.getInfo();\n if(info.network!==config.network || info.signerPubkey!==config.serverKey) throw new Error(\"Ark provider network or signing key changed. Review the wallet configuration.\");\n const identity=MnemonicIdentity.fromMnemonic(mnemonic,{isMainnet:config.network===\"bitcoin\"});\n const preview=new PreviewProvider(config.provider);\n const indexer=new RestIndexerProvider(config.provider);\n const repositories={...storage,walletRepository:storage?.walletRepository??new IndexedDBWalletRepository(`ghostly-ark-${config.walletId}`),contractRepository:storage?.contractRepository??new IndexedDBContractRepository(`ghostly-ark-${config.walletId}`)};\n const wallet=await Wallet.create({identity,arkProvider:preview,indexerProvider:indexer,onchainProvider:new EsploraProvider(config.explorer),\n // Renews VTXOs before they expire and settles on-chain deposits into Ark, so a wallet left alone keeps its funds.\n // A local regtest server expires batches within minutes; there the test drives settlement itself.\n settlementConfig:config.network===\"regtest\"?false:{boardingUtxoSweep:true},walletMode:\"hd\",storage:repositories});\n return new ArkadeAdapter(config,identity,wallet,preview,provider,indexer,repositories.walletRepository);\n }\n async address() { return this.wallet.getAddress(); }\n requestAddress() { return this.serial(async()=>(await this.wallet.getNewAddresses({types:[\"default\"],forceNew:true}))[0].address); }\n async balance() { return (await this.wallet.getBalance()).available; }\n /**\n * Sats whose batch expired before they were renewed, in three kinds, as a person can act on them:\n * - `sweeping`: past their expiry, but the server has not swept their batch yet (with any swept ones: none can be\n * recovered meanwhile). The SDK (0.4.76) counts them as\n * recoverable already, but it settles an unswept coin without the forfeit the server still asks for (arkd wants\n * one until the sweep): the batch fails (\"missing forfeit transactions\") and the server bans the coin for a while.\n * The sweep waits for the chain's time to pass the expiry: an hour or more on a real network, the expiry's blocks\n * on regtest.\n * - `recoverable`: swept, and a recovery takes them now.\n * - `small`: swept, but together under the server's dust limit: a recovery makes one new coin and the server makes\n * none that small, so `recoverVtxos` answers \"No recoverable VTXOs found\". They wait for more coins to expire.\n */\n async expired() {\n const now={timestamp:new Date()};\n let swept=0,sweeping=0;\n for(const vtxo of await this.wallet.getVtxos({withRecoverable:true,withUnrolled:false})) {\n if(isRecoverable(vtxo))swept+=vtxo.value;\n else if(!vtxo.isSwept&&canRecoverOnchain(vtxo,now))sweeping+=vtxo.value;\n }\n // What a recovery would hand back, over the same coins it would take (only asked when none is still sweeping),\n // net of the server's fees: `recoverVtxos` refuses under the dust limit, where this still answers above 0.\n const takes=!sweeping&&swept>0&&(await (await this.wallet.getVtxoManager()).getRecoverableBalance()).recoverable>=this.wallet.dustAmount;\n // While any waits for the sweep, none can be recovered: all of them wait.\n return sweeping?{recoverable:0,sweeping:sweeping+swept,small:0}:{recoverable:takes?swept:0,sweeping:0,small:takes?0:swept};\n }\n /**\n * When the first coin still spendable expires (ms), or undefined with none: a coin left alone past it is swept by the\n * server and comes back only through a recovery. An expiry in blocks is read against the explorer's tip, at about ten\n * minutes a block.\n */\n async expiresAt():Promise<number|undefined> {\n let at:number|undefined, height:number|undefined;\n for(const vtxo of await this.wallet.getVtxos({withRecoverable:false,withUnrolled:false})) {\n if(vtxo.isSwept||vtxo.isSpent)continue;\n if(vtxo.expiresAt instanceof Date){const ms=vtxo.expiresAt.getTime();if(Number.isFinite(ms))at=Math.min(at??ms,ms);}\n else if(typeof vtxo.expiresAtHeight===\"number\")height=Math.min(height??vtxo.expiresAtHeight,vtxo.expiresAtHeight);\n }\n if(height!==undefined){\n const tip=(await new EsploraProvider(this.config.explorer).getChainTip()).height;\n const ms=Date.now()+Math.max(0,height-tip)*600_000;\n at=Math.min(at??ms,ms);\n }\n return at;\n }\n /**\n * Moves swept outputs back into the balance, through the server's next batch. Refused while any expired coin is\n * not swept yet (`recoverVtxos` would name it too, and that batch fails for all of them), and when what is swept\n * is too little for a batch.\n */\n recover() { return this.serial(async()=>{\n const {recoverable,sweeping,small}=await this.expired();\n if(sweeping)throw new Error(\"Expired coins can be recovered once the Ark server has swept their batch. Try again later.\");\n if(small)throw new Error(\"Too few expired sats to recover on their own. They are recovered with the next coins that expire.\");\n if(!recoverable)throw new Error(\"Nothing to recover\");\n return (await this.wallet.getVtxoManager()).recoverVtxos();\n }); }\n /** On-chain deposits land here; the SDK's settlement moves them into Ark once confirmed. */\n boardingAddress() { return this.wallet.getBoardingAddress(); }\n /** Sats sent on-chain to the boarding address that are not in Ark yet. */\n async incoming() { return (await this.wallet.getBalance()).boarding.total; }\n async dispose() { await this.wallet.dispose(); }\n private async prepareOperation(target:PaymentTarget,amount:number,feeCap:number):Promise<{fee:number;prepared:ArkPrepared}> {\n validatePaymentTarget(target); assertWholeSats(amount); this.checkTarget(target);\n const info=await this.provider.getInfo();\n if(info.network!==this.config.network || info.signerPubkey!==this.config.serverKey)throw new Error(\"Ark provider configuration changed\");\n if(await this.balance()<amount)throw new Error(\"Insufficient Ark balance\");\n this.preview.captured=undefined;\n try { await this.wallet.send({address:target.address,amount}); }\n catch(error) {\n if(!(error instanceof PreparedOnly)) {\n // Provider errors may embed signed payment material; expose only this bounded message.\n // eslint-disable-next-line preserve-caught-error\n throw new Error(\"Could not prepare this Ark payment. Check funds, address and provider.\");\n }\n }\n const capture=this.preview.take();\n if(!capture)throw new Error(\"The Ark adapter did not prepare a transaction\");\n const tx=Transaction.fromPSBT(unbase64(capture.signedTx));\n const destination=ArkAddress.decode(target.address);\n let received=0n,input=0n,output=0n;\n for(let i=0;i<tx.inputsLength;i++){const prev=tx.getInput(i).witnessUtxo;if(!prev)throw new Error(\"Missing Ark input amount\");input+=prev.amount;}\n for(let i=0;i<tx.outputsLength;i++){const out=tx.getOutput(i);output+=out.amount??0n;if(out.script && [hex(destination.pkScript),hex(destination.subdustPkScript)].includes(hex(out.script)))received+=out.amount??0n;}\n const fee=Number(input-output);\n if(received!==BigInt(amount) || !Number.isSafeInteger(fee) || fee<0 || fee>feeCap)throw new Error(\"Prepared amount or fee does not match your review\");\n return {fee,prepared:{...capture,txid:tx.id,address:target.address,amount,fee}};\n }\n private async executeOperation(review:PaymentReview,p:ArkPrepared,persist?:()=>Promise<void>) {\n this.checkPrepared(review,p);\n const info=await this.provider.getInfo();\n if(info.network!==this.config.network || info.signerPubkey!==this.config.serverKey)throw new Error(\"Ark provider configuration changed\");\n const tx=Transaction.fromPSBT(unbase64(p.signedTx));\n const response=await this.provider.submitTx(p.signedTx,p.checkpoints);\n assertSubmittedArkTxid(response,tx,\"Ghostly Ark submission\");\n if(!response.finalArkTx)throw new Error(\"Provider returned no signed transaction\");\n this.verifyServer(Transaction.fromPSBT(unbase64(response.finalArkTx)),tx);\n const matched=matchServerCheckpoints(response.signedCheckpointTxs,p.checkpoints.map(c=>Transaction.fromPSBT(unbase64(c))),\"Ghostly Ark checkpoints\");\n const contracts=await (await this.wallet.getContractManager()).getContracts();\n const final=await Promise.all(matched.map(async ({server,local})=>{\n assertAllowedSighashTypes(server);this.verifyServer(server,local);\n let signed=server;\n for(let index=0;index<local.inputsLength;index++){\n const script=local.getInput(index).witnessUtxo?.script;\n const contract=script && contracts.find(c=>c.script===hex(script));\n if(!contract || contract.type!==\"default\")throw new Error(\"Checkpoint is not owned by this wallet profile\");\n const descriptor=contract.metadata?.signingDescriptor;\n const signer=typeof descriptor===\"string\" ? await this.wallet.signerForDescriptor(descriptor) : contract.params.pubKey===hex(await this.identity.xOnlyPublicKey()) ? this.identity : undefined;\n if(!signer)throw new Error(\"Checkpoint signing descriptor missing\");\n signed=await signer.sign(signed,[index]);\n }\n return base64(signed.toPSBT());\n }));\n p.finalCheckpoints=final;\n await persist?.();\n await this.provider.finalizeTx(p.txid,final);\n return {txid:p.txid,settled:true};\n }\n private async reconcileOperation(review:PaymentReview,p:ArkPrepared,persist?:()=>Promise<void>) {\n this.checkPrepared(review,p);\n if(await this.verifyReceipt(p.txid,p.address,p.amount))return {txid:p.txid,settled:true};\n if(!p.finalCheckpoints){\n // An empty, lagging indexer result can clear the SDK hint. The durable\n // Ghostly journal is authoritative that this approved attempt is unresolved.\n const state=await this.repository.getWalletState();\n if(!state)throw new Error(\"Ark wallet state is unavailable\");\n await this.repository.saveWalletState({...state,settings:{...state.settings,hasPendingTx:true}});\n // Ask for pending transactions using SDK ownership proofs. Only the previously\n // approved transaction may finalize; submitTx remains permanently blocked here.\n this.preview.authorizedFinalize={txid:p.txid,run:async checkpoints=>{\n const matched=matchServerCheckpoints(checkpoints,p.checkpoints.map(c=>Transaction.fromPSBT(unbase64(c))),\"Ghostly pending recovery\");\n for(const {server,local} of matched){assertAllowedSighashTypes(server);this.verifyServer(server,local);}\n p.finalCheckpoints=checkpoints;await persist?.();\n await this.provider.finalizeTx(p.txid,checkpoints);\n }};\n try {await this.wallet.finalizePendingTxs();}finally{this.preview.authorizedFinalize=undefined;}\n }\n // Resume only the same finalization, never a new send/submit after ambiguity.\n if(p.finalCheckpoints) {\n try {await this.provider.finalizeTx(p.txid,p.finalCheckpoints);}\n catch { /* A finalized transaction can reject repeated finalization; verify its receipt. */ }\n }\n return {txid:p.txid,settled:await this.verifyReceipt(p.txid,p.address,p.amount)};\n }\n /**\n * A virtual output paying `address` at least `amount`, made after `since` and none of `claimed`: how a\n * request paid from another Ark wallet is seen, with no receipt from the payer.\n */\n async received(address:string,amount:number,since:number,claimed:ReadonlySet<string>):Promise<string|undefined> {\n const destination=ArkAddress.decode(address);\n const scripts=[hex(destination.pkScript),hex(destination.subdustPkScript)];\n const {vtxos}=await this.indexer.getVtxos({scripts});\n return vtxos.find(v=>/^[a-f0-9]{64}$/.test(v.txid) && v.value>=amount && scripts.includes(v.script) && !claimed.has(v.txid) && v.createdAt.getTime()>=since-60_000)?.txid;\n }\n async verifyReceipt(txid:string,address:string,amount:number):Promise<boolean> {\n if(!/^[a-f0-9]{64}$/.test(txid))return false;\n const destination=ArkAddress.decode(address);\n const result=await this.indexer.getVtxos({outpoints:[{txid,vout:0}]});\n return result.vtxos.some(v=>v.txid===txid && v.value===amount && [hex(destination.pkScript),hex(destination.subdustPkScript)].includes(v.script));\n }\n private checkTarget(t:PaymentTarget) {\n if(t.method!==this.method || t.provider!==this.config.provider || t.network!==this.config.network || t.asset!==\"BTC\" || t.unit!==\"sat\")throw new Error(\"Payment method, network or provider does not match this wallet\");\n const decoded=ArkAddress.decode(t.address);\n const pinned=this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/,\"\");\n if(hex(decoded.serverPubKey)!==pinned || decoded.hrp!==(this.config.network===\"bitcoin\"?\"ark\":\"tark\"))throw new Error(\"Ark address belongs to another provider or network\");\n }\n private verifyServer(server:Transaction,local:Transaction) {\n const key=this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/,\"\");\n for(let index=0;index<local.inputsLength;index++){\n const leaf=local.getInput(index).tapLeafScript?.[0]?.[1];\n if(!leaf)throw new Error(\"Missing reviewed spend leaf\");\n verifyTapscriptSignatures(server,index,[key],undefined,undefined,tapLeafHash(leaf.subarray(0,-1),leaf[leaf.length-1]));\n }\n }\n private checkPrepared(r:PaymentReview,p:ArkPrepared) {\n this.checkTarget(r);\n if(p.address!==r.address || p.amount!==r.amount || p.fee!==r.fee || p.fee>r.feeCap || Transaction.fromPSBT(unbase64(p.signedTx)).id!==p.txid)throw new Error(\"Prepared payment does not match the approved review\");\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"arkade-C_qDmHnm.js","names":[],"sources":["../../../browser/src/engine/paymentAdapters/arkade.ts"],"sourcesContent":["import {\n Wallet, MnemonicIdentity, RestArkProvider, RestIndexerProvider, EsploraProvider,\n IndexedDBWalletRepository, IndexedDBContractRepository, ArkAddress, Transaction,\n assertSubmittedArkTxid, matchServerCheckpoints, assertAllowedSighashTypes, verifyTapscriptSignatures,\n isRecoverable, canRecoverOnchain, type StorageConfig,\n} from \"@arkade-os/sdk\";\nimport { tapLeafHash } from \"@scure/btc-signer/payment.js\";\nimport { assertWholeSats, validatePaymentTarget, type PaymentAdapter, type PaymentReview, type PaymentTarget } from \"@ghostly/core\";\n\nexport interface ArkConfig { network: \"bitcoin\" | \"regtest\" | \"signet\" | \"mutinynet\"; provider: string; explorer: string; serverKey: string; walletId: string }\nexport interface ArkPrepared { signedTx: string; checkpoints: string[]; txid: string; address: string; amount: number; fee: number; finalCheckpoints?: string[] }\nconst unbase64 = (s: string) => Uint8Array.from(atob(s),c=>c.charCodeAt(0));\nconst base64 = (bytes: Uint8Array) => btoa(Array.from(bytes,b=>String.fromCharCode(b)).join(\"\"));\nconst hex = (bytes: Uint8Array) => Array.from(bytes,b=>b.toString(16).padStart(2,\"0\")).join(\"\");\nclass PreparedOnly extends Error {}\nexport const ARK_NETWORKS: ArkConfig[\"network\"][] = [\"bitcoin\", \"mutinynet\", \"signet\", \"regtest\"];\n/** Payments go through review and approval; only the SDK's own settlement (renewal, onboarding) reaches the provider directly. */\nclass PreviewProvider extends RestArkProvider {\n authorizedFinalize?:{txid:string;run:(checkpoints:string[])=>Promise<void>};\n captured?: { signedTx:string; checkpoints:string[] };\n take() { return this.captured; }\n override async submitTx(signedTx:string,checkpoints:string[]):Promise<never> { this.captured={signedTx,checkpoints};throw new PreparedOnly(); }\n override async finalizeTx(txid:string,checkpoints:string[]):Promise<void> {\n if(this.authorizedFinalize?.txid!==txid)throw new Error(\"Preview cannot finalize an unapproved payment\");\n await this.authorizedFinalize.run(checkpoints);\n }\n}\nexport class ArkadeAdapter implements PaymentAdapter<ArkPrepared> {\n readonly method = \"arkade\" as const;\n private pending:Promise<unknown>=Promise.resolve();\n private serial<T>(run:()=>Promise<T>):Promise<T>{const next=this.pending.then(run,run);this.pending=next.catch(()=>{});return next;}\n prepare(target:PaymentTarget,amount:number,feeCap:number){return this.serial(()=>this.prepareOperation(target,amount,feeCap));}\n execute(review:PaymentReview,prepared:ArkPrepared,persist?:()=>Promise<void>){return this.serial(()=>this.executeOperation(review,prepared,persist));}\n reconcile(review:PaymentReview,prepared:ArkPrepared,persist?:()=>Promise<void>){return this.serial(()=>this.reconcileOperation(review,prepared,persist));}\n private constructor(readonly config: ArkConfig, private identity: MnemonicIdentity, private wallet: Wallet, private preview: PreviewProvider, private provider: RestArkProvider, private indexer: RestIndexerProvider,private repository:NonNullable<StorageConfig[\"walletRepository\"]>) {}\n static async connect(config: ArkConfig, mnemonic: string, storage?: StorageConfig): Promise<ArkadeAdapter> {\n if (!ARK_NETWORKS.includes(config.network)) throw new Error(\"Unsupported Ark network\");\n validatePaymentTarget({method:\"arkade\",network:config.network,provider:config.provider,asset:\"BTC\",unit:\"sat\",address:\"configuration\",expiresAt:Date.now()+60000});\n validatePaymentTarget({method:\"arkade\",network:config.network,provider:config.explorer,asset:\"BTC\",unit:\"sat\",address:\"configuration\",expiresAt:Date.now()+60000});\n const provider=new RestArkProvider(config.provider);\n const info=await provider.getInfo();\n if(info.network!==config.network || info.signerPubkey!==config.serverKey) throw new Error(\"Ark provider network or signing key changed. Review the wallet configuration.\");\n const identity=MnemonicIdentity.fromMnemonic(mnemonic,{isMainnet:config.network===\"bitcoin\"});\n const preview=new PreviewProvider(config.provider);\n const indexer=new RestIndexerProvider(config.provider);\n const repositories={...storage,walletRepository:storage?.walletRepository??new IndexedDBWalletRepository(`ghostly-ark-${config.walletId}`),contractRepository:storage?.contractRepository??new IndexedDBContractRepository(`ghostly-ark-${config.walletId}`)};\n const wallet=await Wallet.create({identity,arkProvider:preview,indexerProvider:indexer,onchainProvider:new EsploraProvider(config.explorer),\n // Renews VTXOs before they expire and settles on-chain deposits into Ark, so a wallet left alone keeps its funds.\n // A local regtest server expires batches within minutes; there the test drives settlement itself.\n settlementConfig:config.network===\"regtest\"?false:{boardingUtxoSweep:true},walletMode:\"hd\",storage:repositories});\n return new ArkadeAdapter(config,identity,wallet,preview,provider,indexer,repositories.walletRepository);\n }\n async address() { return this.wallet.getAddress(); }\n requestAddress() { return this.serial(async()=>(await this.wallet.getNewAddresses({types:[\"default\"],forceNew:true}))[0].address); }\n async balance() { return (await this.wallet.getBalance()).available; }\n /**\n * Sats whose batch expired before they were renewed, in three kinds, as a person can act on them:\n * - `sweeping`: past their expiry, but the server has not swept their batch yet (with any swept ones: none can be\n * recovered meanwhile). The SDK (0.4.76) counts them as\n * recoverable already, but it settles an unswept coin without the forfeit the server still asks for (arkd wants\n * one until the sweep): the batch fails (\"missing forfeit transactions\") and the server bans the coin for a while.\n * The sweep waits for the chain's time to pass the expiry: an hour or more on a real network, the expiry's blocks\n * on regtest.\n * - `recoverable`: swept, and a recovery takes them now.\n * - `small`: swept, but together under the server's dust limit: a recovery makes one new coin and the server makes\n * none that small, so `recoverVtxos` answers \"No recoverable VTXOs found\". They wait for more coins to expire.\n */\n async expired() {\n const now={timestamp:new Date()};\n let swept=0,sweeping=0;\n for(const vtxo of await this.wallet.getVtxos({withRecoverable:true,withUnrolled:false})) {\n if(isRecoverable(vtxo))swept+=vtxo.value;\n else if(!vtxo.isSwept&&canRecoverOnchain(vtxo,now))sweeping+=vtxo.value;\n }\n // What a recovery would hand back, over the same coins it would take (only asked when none is still sweeping),\n // net of the server's fees: `recoverVtxos` refuses under the dust limit, where this still answers above 0.\n const takes=!sweeping&&swept>0&&(await (await this.wallet.getVtxoManager()).getRecoverableBalance()).recoverable>=this.wallet.dustAmount;\n // While any waits for the sweep, none can be recovered: all of them wait.\n return sweeping?{recoverable:0,sweeping:sweeping+swept,small:0}:{recoverable:takes?swept:0,sweeping:0,small:takes?0:swept};\n }\n /**\n * When the first coin still spendable expires (ms), or undefined with none: a coin left alone past it is swept by the\n * server and comes back only through a recovery. An expiry in blocks is read against the explorer's tip, at about ten\n * minutes a block.\n */\n async expiresAt():Promise<number|undefined> {\n let at:number|undefined, height:number|undefined;\n for(const vtxo of await this.wallet.getVtxos({withRecoverable:false,withUnrolled:false})) {\n if(vtxo.isSwept||vtxo.isSpent)continue;\n if(vtxo.expiresAt instanceof Date){const ms=vtxo.expiresAt.getTime();if(Number.isFinite(ms))at=Math.min(at??ms,ms);}\n else if(typeof vtxo.expiresAtHeight===\"number\")height=Math.min(height??vtxo.expiresAtHeight,vtxo.expiresAtHeight);\n }\n if(height!==undefined){\n const tip=(await new EsploraProvider(this.config.explorer).getChainTip()).height;\n const ms=Date.now()+Math.max(0,height-tip)*600_000;\n at=Math.min(at??ms,ms);\n }\n return at;\n }\n /**\n * Moves swept outputs back into the balance, through the server's next batch. Refused while any expired coin is\n * not swept yet (`recoverVtxos` would name it too, and that batch fails for all of them), and when what is swept\n * is too little for a batch.\n */\n recover() { return this.serial(async()=>{\n const {recoverable,sweeping,small}=await this.expired();\n if(sweeping)throw new Error(\"Expired coins can be recovered once the Ark server has swept their batch. Try again later.\");\n if(small)throw new Error(\"Too few expired sats to recover on their own. They are recovered with the next coins that expire.\");\n if(!recoverable)throw new Error(\"Nothing to recover\");\n return (await this.wallet.getVtxoManager()).recoverVtxos();\n }); }\n /** On-chain deposits land here; the SDK's settlement moves them into Ark once confirmed. */\n boardingAddress() { return this.wallet.getBoardingAddress(); }\n /** Sats sent on-chain to the boarding address that are not in Ark yet. */\n async incoming() { return (await this.wallet.getBalance()).boarding.total; }\n async dispose() { await this.wallet.dispose(); }\n private async prepareOperation(target:PaymentTarget,amount:number,feeCap:number):Promise<{fee:number;prepared:ArkPrepared}> {\n validatePaymentTarget(target); assertWholeSats(amount); this.checkTarget(target);\n const info=await this.provider.getInfo();\n if(info.network!==this.config.network || info.signerPubkey!==this.config.serverKey)throw new Error(\"Ark provider configuration changed\");\n if(await this.balance()<amount)throw new Error(\"Insufficient Ark balance\");\n this.preview.captured=undefined;\n try { await this.wallet.send({address:target.address,amount}); }\n catch(error) {\n if(!(error instanceof PreparedOnly)) {\n // Provider errors may embed signed payment material; expose only this bounded message.\n // eslint-disable-next-line preserve-caught-error\n throw new Error(\"Could not prepare this Ark payment. Check funds, address and provider.\");\n }\n }\n const capture=this.preview.take();\n if(!capture)throw new Error(\"The Ark adapter did not prepare a transaction\");\n const tx=Transaction.fromPSBT(unbase64(capture.signedTx));\n const destination=ArkAddress.decode(target.address);\n let received=0n,input=0n,output=0n;\n for(let i=0;i<tx.inputsLength;i++){const prev=tx.getInput(i).witnessUtxo;if(!prev)throw new Error(\"Missing Ark input amount\");input+=prev.amount;}\n for(let i=0;i<tx.outputsLength;i++){const out=tx.getOutput(i);output+=out.amount??0n;if(out.script && [hex(destination.pkScript),hex(destination.subdustPkScript)].includes(hex(out.script)))received+=out.amount??0n;}\n const fee=Number(input-output);\n if(received!==BigInt(amount) || !Number.isSafeInteger(fee) || fee<0 || fee>feeCap)throw new Error(\"Prepared amount or fee does not match your review\");\n return {fee,prepared:{...capture,txid:tx.id,address:target.address,amount,fee}};\n }\n private async executeOperation(review:PaymentReview,p:ArkPrepared,persist?:()=>Promise<void>) {\n this.checkPrepared(review,p);\n const info=await this.provider.getInfo();\n if(info.network!==this.config.network || info.signerPubkey!==this.config.serverKey)throw new Error(\"Ark provider configuration changed\");\n const tx=Transaction.fromPSBT(unbase64(p.signedTx));\n const response=await this.provider.submitTx(p.signedTx,p.checkpoints);\n assertSubmittedArkTxid(response,tx,\"Ghostly Ark submission\");\n if(!response.finalArkTx)throw new Error(\"Provider returned no signed transaction\");\n this.verifyServer(Transaction.fromPSBT(unbase64(response.finalArkTx)),tx);\n const matched=matchServerCheckpoints(response.signedCheckpointTxs,p.checkpoints.map(c=>Transaction.fromPSBT(unbase64(c))),\"Ghostly Ark checkpoints\");\n const contracts=await (await this.wallet.getContractManager()).getContracts();\n const final=await Promise.all(matched.map(async ({server,local})=>{\n assertAllowedSighashTypes(server);this.verifyServer(server,local);\n let signed=server;\n for(let index=0;index<local.inputsLength;index++){\n const script=local.getInput(index).witnessUtxo?.script;\n const contract=script && contracts.find(c=>c.script===hex(script));\n if(!contract || contract.type!==\"default\")throw new Error(\"Checkpoint is not owned by this wallet profile\");\n const descriptor=contract.metadata?.signingDescriptor;\n const signer=typeof descriptor===\"string\" ? await this.wallet.signerForDescriptor(descriptor) : contract.params.pubKey===hex(await this.identity.xOnlyPublicKey()) ? this.identity : undefined;\n if(!signer)throw new Error(\"Checkpoint signing descriptor missing\");\n signed=await signer.sign(signed,[index]);\n }\n return base64(signed.toPSBT());\n }));\n p.finalCheckpoints=final;\n await persist?.();\n await this.provider.finalizeTx(p.txid,final);\n return {txid:p.txid,settled:true};\n }\n private async reconcileOperation(review:PaymentReview,p:ArkPrepared,persist?:()=>Promise<void>) {\n this.checkPrepared(review,p);\n if(await this.verifyReceipt(p.txid,p.address,p.amount))return {txid:p.txid,settled:true};\n if(!p.finalCheckpoints){\n // An empty, lagging indexer result can clear the SDK hint. The durable\n // Ghostly journal is authoritative that this approved attempt is unresolved.\n const state=await this.repository.getWalletState();\n if(!state)throw new Error(\"Ark wallet state is unavailable\");\n await this.repository.saveWalletState({...state,settings:{...state.settings,hasPendingTx:true}});\n // Ask for pending transactions using SDK ownership proofs. Only the previously\n // approved transaction may finalize; submitTx remains permanently blocked here.\n this.preview.authorizedFinalize={txid:p.txid,run:async checkpoints=>{\n const matched=matchServerCheckpoints(checkpoints,p.checkpoints.map(c=>Transaction.fromPSBT(unbase64(c))),\"Ghostly pending recovery\");\n for(const {server,local} of matched){assertAllowedSighashTypes(server);this.verifyServer(server,local);}\n p.finalCheckpoints=checkpoints;await persist?.();\n await this.provider.finalizeTx(p.txid,checkpoints);\n }};\n try {await this.wallet.finalizePendingTxs();}finally{this.preview.authorizedFinalize=undefined;}\n }\n // Resume only the same finalization, never a new send/submit after ambiguity.\n if(p.finalCheckpoints) {\n try {await this.provider.finalizeTx(p.txid,p.finalCheckpoints);}\n catch { /* A finalized transaction can reject repeated finalization; verify its receipt. */ }\n }\n return {txid:p.txid,settled:await this.verifyReceipt(p.txid,p.address,p.amount)};\n }\n /**\n * A virtual output paying `address` at least `amount`, made after `since` and none of `claimed`: how a\n * request paid from another Ark wallet is seen, with no receipt from the payer.\n */\n async received(address:string,amount:number,since:number,claimed:ReadonlySet<string>):Promise<string|undefined> {\n const destination=ArkAddress.decode(address);\n const scripts=[hex(destination.pkScript),hex(destination.subdustPkScript)];\n const {vtxos}=await this.indexer.getVtxos({scripts});\n return vtxos.find(v=>/^[a-f0-9]{64}$/.test(v.txid) && v.value>=amount && scripts.includes(v.script) && !claimed.has(v.txid) && v.createdAt.getTime()>=since-60_000)?.txid;\n }\n async verifyReceipt(txid:string,address:string,amount:number):Promise<boolean> {\n if(!/^[a-f0-9]{64}$/.test(txid))return false;\n const destination=ArkAddress.decode(address);\n const result=await this.indexer.getVtxos({outpoints:[{txid,vout:0}]});\n return result.vtxos.some(v=>v.txid===txid && v.value===amount && [hex(destination.pkScript),hex(destination.subdustPkScript)].includes(v.script));\n }\n private checkTarget(t:PaymentTarget) {\n if(t.method!==this.method || t.provider!==this.config.provider || t.network!==this.config.network || t.asset!==\"BTC\" || t.unit!==\"sat\")throw new Error(\"Payment method, network or provider does not match this wallet\");\n const decoded=ArkAddress.decode(t.address);\n const pinned=this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/,\"\");\n if(hex(decoded.serverPubKey)!==pinned || decoded.hrp!==(this.config.network===\"bitcoin\"?\"ark\":\"tark\"))throw new Error(\"Ark address belongs to another provider or network\");\n }\n private verifyServer(server:Transaction,local:Transaction) {\n const key=this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/,\"\");\n for(let index=0;index<local.inputsLength;index++){\n const leaf=local.getInput(index).tapLeafScript?.[0]?.[1];\n if(!leaf)throw new Error(\"Missing reviewed spend leaf\");\n verifyTapscriptSignatures(server,index,[key],undefined,undefined,tapLeafHash(leaf.subarray(0,-1),leaf[leaf.length-1]));\n }\n }\n private checkPrepared(r:PaymentReview,p:ArkPrepared) {\n this.checkTarget(r);\n if(p.address!==r.address || p.amount!==r.amount || p.fee!==r.fee || p.fee>r.feeCap || Transaction.fromPSBT(unbase64(p.signedTx)).id!==p.txid)throw new Error(\"Prepared payment does not match the approved review\");\n }\n}\n"],"mappings":";;;;AAWA,IAAM,YAAY,MAAc,WAAW,KAAK,KAAK,CAAC,IAAE,MAAG,EAAE,WAAW,CAAC,CAAC;AAC1E,IAAM,UAAU,UAAsB,KAAK,MAAM,KAAK,QAAM,MAAG,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;AAC/F,IAAM,OAAO,UAAsB,MAAM,KAAK,QAAM,MAAG,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAE,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AAC9F,IAAM,eAAN,cAA2B,MAAM,CAAC;AAClC,IAAa,eAAuC;CAAC;CAAW;CAAa;CAAU;AAAS;;AAEhG,IAAM,kBAAN,cAA8B,gBAAgB;CAC5C;CACA;CACA,OAAO;EAAE,OAAO,KAAK;CAAU;CAC/B,MAAe,SAAS,UAAgB,aAAqC;EAAE,KAAK,WAAS;GAAC;GAAS;EAAW;EAAE,MAAM,IAAI,aAAa;CAAG;CAC9I,MAAe,WAAW,MAAY,aAAoC;EACxE,IAAG,KAAK,oBAAoB,SAAO,MAAK,MAAM,IAAI,MAAM,+CAA+C;EACvG,MAAM,KAAK,mBAAmB,IAAI,WAAW;CAC/C;AACF;AACA,IAAa,gBAAb,MAAa,cAAqD;CAOnC;CAA2B;CAAoC;CAAwB;CAAkC;CAAmC;CAAqC;CAN9N,SAAkB;CAClB,UAAiC,QAAQ,QAAQ;CACjD,OAAkB,KAA8B;EAAC,MAAM,OAAK,KAAK,QAAQ,KAAK,KAAI,GAAG;EAAE,KAAK,UAAQ,KAAK,YAAU,CAAC,CAAC;EAAE,OAAO;CAAK;CACnI,QAAQ,QAAqB,QAAc,QAAc;EAAC,OAAO,KAAK,aAAW,KAAK,iBAAiB,QAAO,QAAO,MAAM,CAAC;CAAE;CAC9H,QAAQ,QAAqB,UAAqB,SAA2B;EAAC,OAAO,KAAK,aAAW,KAAK,iBAAiB,QAAO,UAAS,OAAO,CAAC;CAAE;CACrJ,UAAU,QAAqB,UAAqB,SAA2B;EAAC,OAAO,KAAK,aAAW,KAAK,mBAAmB,QAAO,UAAS,OAAO,CAAC;CAAE;CACzJ,YAAoB,QAA4B,UAAoC,QAAwB,SAAkC,UAAmC,SAAqC,YAAmE;EAA5P,KAAA,SAAA;EAA2B,KAAA,WAAA;EAAoC,KAAA,SAAA;EAAwB,KAAA,UAAA;EAAkC,KAAA,WAAA;EAAmC,KAAA,UAAA;EAAqC,KAAA,aAAA;CAA4D;CAC1R,aAAa,QAAQ,QAAmB,UAAkB,SAAiD;EACzG,IAAI,CAAC,aAAa,SAAS,OAAO,OAAO,GAAG,MAAM,IAAI,MAAM,yBAAyB;EACrF,sBAAsB;GAAC,QAAO;GAAS,SAAQ,OAAO;GAAQ,UAAS,OAAO;GAAS,OAAM;GAAM,MAAK;GAAM,SAAQ;GAAgB,WAAU,KAAK,IAAI,IAAE;EAAK,CAAC;EACjK,sBAAsB;GAAC,QAAO;GAAS,SAAQ,OAAO;GAAQ,UAAS,OAAO;GAAS,OAAM;GAAM,MAAK;GAAM,SAAQ;GAAgB,WAAU,KAAK,IAAI,IAAE;EAAK,CAAC;EACjK,MAAM,WAAS,IAAI,gBAAgB,OAAO,QAAQ;EAClD,MAAM,OAAK,MAAM,SAAS,QAAQ;EAClC,IAAG,KAAK,YAAU,OAAO,WAAW,KAAK,iBAAe,OAAO,WAAW,MAAM,IAAI,MAAM,+EAA+E;EACzK,MAAM,WAAS,iBAAiB,aAAa,UAAS,EAAC,WAAU,OAAO,YAAU,UAAS,CAAC;EAC5F,MAAM,UAAQ,IAAI,gBAAgB,OAAO,QAAQ;EACjD,MAAM,UAAQ,IAAI,oBAAoB,OAAO,QAAQ;EACrD,MAAM,eAAa;GAAC,GAAG;GAAQ,kBAAiB,SAAS,oBAAkB,IAAI,0BAA0B,eAAe,OAAO,UAAU;GAAE,oBAAmB,SAAS,sBAAoB,IAAI,4BAA4B,eAAe,OAAO,UAAU;EAAC;EAC5P,MAAM,SAAO,MAAM,OAAO,OAAO;GAAC;GAAS,aAAY;GAAQ,iBAAgB;GAAQ,iBAAgB,IAAI,gBAAgB,OAAO,QAAQ;GAGxI,kBAAiB,OAAO,YAAU,YAAU,QAAM,EAAC,mBAAkB,KAAI;GAAE,YAAW;GAAK,SAAQ;EAAY,CAAC;EAClH,OAAO,IAAI,cAAc,QAAO,UAAS,QAAO,SAAQ,UAAS,SAAQ,aAAa,gBAAgB;CACxG;CACA,MAAM,UAAU;EAAE,OAAO,KAAK,OAAO,WAAW;CAAG;CACnD,iBAAiB;EAAE,OAAO,KAAK,OAAO,aAAU,MAAM,KAAK,OAAO,gBAAgB;GAAC,OAAM,CAAC,SAAS;GAAE,UAAS;EAAI,CAAC,EAAA,CAAG,EAAE,CAAC,OAAO;CAAG;CACnI,MAAM,UAAU;EAAE,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAA,CAAG;CAAW;;;;;;;;;;;;;CAarE,MAAM,UAAU;EACd,MAAM,MAAI,EAAC,2BAAU,IAAI,KAAK,EAAC;EAC/B,IAAI,QAAM,GAAE,WAAS;EACrB,KAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS;GAAC,iBAAgB;GAAK,cAAa;EAAK,CAAC,GACpF,IAAG,cAAc,IAAI,GAAE,SAAO,KAAK;OAC9B,IAAG,CAAC,KAAK,WAAS,kBAAkB,MAAK,GAAG,GAAE,YAAU,KAAK;EAIpE,MAAM,QAAM,CAAC,YAAU,QAAM,MAAI,OAAO,MAAM,KAAK,OAAO,eAAe,EAAA,CAAG,sBAAsB,EAAA,CAAG,eAAa,KAAK,OAAO;EAE9H,OAAO,WAAS;GAAC,aAAY;GAAE,UAAS,WAAS;GAAM,OAAM;EAAC,IAAE;GAAC,aAAY,QAAM,QAAM;GAAE,UAAS;GAAE,OAAM,QAAM,IAAE;EAAK;CAC3H;;;;;;CAMA,MAAM,YAAsC;EAC1C,IAAI,IAAqB;EACzB,KAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS;GAAC,iBAAgB;GAAM,cAAa;EAAK,CAAC,GAAG;GACxF,IAAG,KAAK,WAAS,KAAK,SAAQ;GAC9B,IAAG,KAAK,qBAAqB,MAAK;IAAC,MAAM,KAAG,KAAK,UAAU,QAAQ;IAAE,IAAG,OAAO,SAAS,EAAE,GAAE,KAAG,KAAK,IAAI,MAAI,IAAG,EAAE;GAAE,OAC9G,IAAG,OAAO,KAAK,oBAAkB,UAAS,SAAO,KAAK,IAAI,UAAQ,KAAK,iBAAgB,KAAK,eAAe;EAClH;EACA,IAAG,WAAS,KAAA,GAAU;GACpB,MAAM,OAAK,MAAM,IAAI,gBAAgB,KAAK,OAAO,QAAQ,CAAC,CAAC,YAAY,EAAA,CAAG;GAC1E,MAAM,KAAG,KAAK,IAAI,IAAE,KAAK,IAAI,GAAE,SAAO,GAAG,IAAE;GAC3C,KAAG,KAAK,IAAI,MAAI,IAAG,EAAE;EACvB;EACA,OAAO;CACT;;;;;;CAMA,UAAU;EAAE,OAAO,KAAK,OAAO,YAAS;GACtC,MAAM,EAAC,aAAY,UAAS,UAAO,MAAM,KAAK,QAAQ;GACtD,IAAG,UAAS,MAAM,IAAI,MAAM,4FAA4F;GACxH,IAAG,OAAM,MAAM,IAAI,MAAM,mGAAmG;GAC5H,IAAG,CAAC,aAAY,MAAM,IAAI,MAAM,oBAAoB;GACpD,QAAQ,MAAM,KAAK,OAAO,eAAe,EAAA,CAAG,aAAa;EAC3D,CAAC;CAAG;;CAEJ,kBAAkB;EAAE,OAAO,KAAK,OAAO,mBAAmB;CAAG;;CAE7D,MAAM,WAAW;EAAE,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAA,CAAG,SAAS;CAAO;CAC3E,MAAM,UAAU;EAAE,MAAM,KAAK,OAAO,QAAQ;CAAG;CAC/C,MAAc,iBAAiB,QAAqB,QAAc,QAA0D;EAC1H,sBAAsB,MAAM;EAAG,gBAAgB,MAAM;EAAG,KAAK,YAAY,MAAM;EAC/E,MAAM,OAAK,MAAM,KAAK,SAAS,QAAQ;EACvC,IAAG,KAAK,YAAU,KAAK,OAAO,WAAW,KAAK,iBAAe,KAAK,OAAO,WAAU,MAAM,IAAI,MAAM,oCAAoC;EACvI,IAAG,MAAM,KAAK,QAAQ,IAAE,QAAO,MAAM,IAAI,MAAM,0BAA0B;EACzE,KAAK,QAAQ,WAAS,KAAA;EACtB,IAAI;GAAE,MAAM,KAAK,OAAO,KAAK;IAAC,SAAQ,OAAO;IAAQ;GAAM,CAAC;EAAG,SACzD,OAAO;GACX,IAAG,EAAE,iBAAiB,eAGpB,MAAM,IAAI,MAAM,wEAAwE;EAE5F;EACA,MAAM,UAAQ,KAAK,QAAQ,KAAK;EAChC,IAAG,CAAC,SAAQ,MAAM,IAAI,MAAM,+CAA+C;EAC3E,MAAM,KAAG,YAAY,SAAS,SAAS,QAAQ,QAAQ,CAAC;EACxD,MAAM,cAAY,WAAW,OAAO,OAAO,OAAO;EAClD,IAAI,WAAS,IAAG,QAAM,IAAG,SAAO;EAChC,KAAI,IAAI,IAAE,GAAE,IAAE,GAAG,cAAa,KAAI;GAAC,MAAM,OAAK,GAAG,SAAS,CAAC,CAAC,CAAC;GAAY,IAAG,CAAC,MAAK,MAAM,IAAI,MAAM,0BAA0B;GAAE,SAAO,KAAK;EAAO;EACjJ,KAAI,IAAI,IAAE,GAAE,IAAE,GAAG,eAAc,KAAI;GAAC,MAAM,MAAI,GAAG,UAAU,CAAC;GAAE,UAAQ,IAAI,UAAQ;GAAG,IAAG,IAAI,UAAU,CAAC,IAAI,YAAY,QAAQ,GAAE,IAAI,YAAY,eAAe,CAAC,CAAC,CAAC,SAAS,IAAI,IAAI,MAAM,CAAC,GAAE,YAAU,IAAI,UAAQ;EAAG;EACtN,MAAM,MAAI,OAAO,QAAM,MAAM;EAC7B,IAAG,aAAW,OAAO,MAAM,KAAK,CAAC,OAAO,cAAc,GAAG,KAAK,MAAI,KAAK,MAAI,QAAO,MAAM,IAAI,MAAM,mDAAmD;EACrJ,OAAO;GAAC;GAAI,UAAS;IAAC,GAAG;IAAQ,MAAK,GAAG;IAAG,SAAQ,OAAO;IAAQ;IAAO;GAAG;EAAC;CAChF;CACA,MAAc,iBAAiB,QAAqB,GAAc,SAA4B;EAC5F,KAAK,cAAc,QAAO,CAAC;EAC3B,MAAM,OAAK,MAAM,KAAK,SAAS,QAAQ;EACvC,IAAG,KAAK,YAAU,KAAK,OAAO,WAAW,KAAK,iBAAe,KAAK,OAAO,WAAU,MAAM,IAAI,MAAM,oCAAoC;EACvI,MAAM,KAAG,YAAY,SAAS,SAAS,EAAE,QAAQ,CAAC;EAClD,MAAM,WAAS,MAAM,KAAK,SAAS,SAAS,EAAE,UAAS,EAAE,WAAW;EACpE,uBAAuB,UAAS,IAAG,wBAAwB;EAC3D,IAAG,CAAC,SAAS,YAAW,MAAM,IAAI,MAAM,yCAAyC;EACjF,KAAK,aAAa,YAAY,SAAS,SAAS,SAAS,UAAU,CAAC,GAAE,EAAE;EACxE,MAAM,UAAQ,uBAAuB,SAAS,qBAAoB,EAAE,YAAY,KAAI,MAAG,YAAY,SAAS,SAAS,CAAC,CAAC,CAAC,GAAE,yBAAyB;EACnJ,MAAM,YAAU,OAAO,MAAM,KAAK,OAAO,mBAAmB,EAAA,CAAG,aAAa;EAC5E,MAAM,QAAM,MAAM,QAAQ,IAAI,QAAQ,IAAI,OAAO,EAAC,QAAO,YAAS;GAChE,0BAA0B,MAAM;GAAE,KAAK,aAAa,QAAO,KAAK;GAChE,IAAI,SAAO;GACX,KAAI,IAAI,QAAM,GAAE,QAAM,MAAM,cAAa,SAAQ;IAC/C,MAAM,SAAO,MAAM,SAAS,KAAK,CAAC,CAAC,aAAa;IAChD,MAAM,WAAS,UAAU,UAAU,MAAK,MAAG,EAAE,WAAS,IAAI,MAAM,CAAC;IACjE,IAAG,CAAC,YAAY,SAAS,SAAO,WAAU,MAAM,IAAI,MAAM,gDAAgD;IAC1G,MAAM,aAAW,SAAS,UAAU;IACpC,MAAM,SAAO,OAAO,eAAa,WAAW,MAAM,KAAK,OAAO,oBAAoB,UAAU,IAAI,SAAS,OAAO,WAAS,IAAI,MAAM,KAAK,SAAS,eAAe,CAAC,IAAI,KAAK,WAAW,KAAA;IACrL,IAAG,CAAC,QAAO,MAAM,IAAI,MAAM,uCAAuC;IAClE,SAAO,MAAM,OAAO,KAAK,QAAO,CAAC,KAAK,CAAC;GACzC;GACA,OAAO,OAAO,OAAO,OAAO,CAAC;EAC/B,CAAC,CAAC;EACF,EAAE,mBAAiB;EACnB,MAAM,UAAU;EAChB,MAAM,KAAK,SAAS,WAAW,EAAE,MAAK,KAAK;EAC3C,OAAO;GAAC,MAAK,EAAE;GAAK,SAAQ;EAAI;CAClC;CACA,MAAc,mBAAmB,QAAqB,GAAc,SAA4B;EAC9F,KAAK,cAAc,QAAO,CAAC;EAC3B,IAAG,MAAM,KAAK,cAAc,EAAE,MAAK,EAAE,SAAQ,EAAE,MAAM,GAAE,OAAO;GAAC,MAAK,EAAE;GAAK,SAAQ;EAAI;EACvF,IAAG,CAAC,EAAE,kBAAiB;GAGrB,MAAM,QAAM,MAAM,KAAK,WAAW,eAAe;GACjD,IAAG,CAAC,OAAM,MAAM,IAAI,MAAM,iCAAiC;GAC3D,MAAM,KAAK,WAAW,gBAAgB;IAAC,GAAG;IAAM,UAAS;KAAC,GAAG,MAAM;KAAS,cAAa;IAAI;GAAC,CAAC;GAG/F,KAAK,QAAQ,qBAAmB;IAAC,MAAK,EAAE;IAAK,KAAI,OAAM,gBAAa;KAClE,MAAM,UAAQ,uBAAuB,aAAY,EAAE,YAAY,KAAI,MAAG,YAAY,SAAS,SAAS,CAAC,CAAC,CAAC,GAAE,0BAA0B;KACnI,KAAI,MAAM,EAAC,QAAO,WAAU,SAAQ;MAAC,0BAA0B,MAAM;MAAE,KAAK,aAAa,QAAO,KAAK;KAAE;KACvG,EAAE,mBAAiB;KAAY,MAAM,UAAU;KAC/C,MAAM,KAAK,SAAS,WAAW,EAAE,MAAK,WAAW;IACnD;GAAC;GACD,IAAI;IAAC,MAAM,KAAK,OAAO,mBAAmB;GAAE,UAAQ;IAAC,KAAK,QAAQ,qBAAmB,KAAA;GAAU;EACjG;EAEA,IAAG,EAAE,kBACH,IAAI;GAAC,MAAM,KAAK,SAAS,WAAW,EAAE,MAAK,EAAE,gBAAgB;EAAE,QACzD,CAAsF;EAE9F,OAAO;GAAC,MAAK,EAAE;GAAK,SAAQ,MAAM,KAAK,cAAc,EAAE,MAAK,EAAE,SAAQ,EAAE,MAAM;EAAC;CACjF;;;;;CAKA,MAAM,SAAS,SAAe,QAAc,OAAa,SAAuD;EAC9G,MAAM,cAAY,WAAW,OAAO,OAAO;EAC3C,MAAM,UAAQ,CAAC,IAAI,YAAY,QAAQ,GAAE,IAAI,YAAY,eAAe,CAAC;EACzE,MAAM,EAAC,UAAO,MAAM,KAAK,QAAQ,SAAS,EAAC,QAAO,CAAC;EACnD,OAAO,MAAM,MAAK,MAAG,iBAAiB,KAAK,EAAE,IAAI,KAAK,EAAE,SAAO,UAAU,QAAQ,SAAS,EAAE,MAAM,KAAK,CAAC,QAAQ,IAAI,EAAE,IAAI,KAAK,EAAE,UAAU,QAAQ,KAAG,QAAM,GAAM,CAAC,EAAE;CACvK;CACA,MAAM,cAAc,MAAY,SAAe,QAAgC;EAC7E,IAAG,CAAC,iBAAiB,KAAK,IAAI,GAAE,OAAO;EACvC,MAAM,cAAY,WAAW,OAAO,OAAO;EAE3C,QAAO,MADY,KAAK,QAAQ,SAAS,EAAC,WAAU,CAAC;GAAC;GAAK,MAAK;EAAC,CAAC,EAAC,CAAC,EAAA,CACtD,MAAM,MAAK,MAAG,EAAE,SAAO,QAAQ,EAAE,UAAQ,UAAU,CAAC,IAAI,YAAY,QAAQ,GAAE,IAAI,YAAY,eAAe,CAAC,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC;CAClJ;CACA,YAAoB,GAAiB;EACnC,IAAG,EAAE,WAAS,KAAK,UAAU,EAAE,aAAW,KAAK,OAAO,YAAY,EAAE,YAAU,KAAK,OAAO,WAAW,EAAE,UAAQ,SAAS,EAAE,SAAO,OAAM,MAAM,IAAI,MAAM,gEAAgE;EACvN,MAAM,UAAQ,WAAW,OAAO,EAAE,OAAO;EACzC,MAAM,SAAO,KAAK,OAAO,UAAU,QAAQ,6BAA4B,EAAE;EACzE,IAAG,IAAI,QAAQ,YAAY,MAAI,UAAU,QAAQ,SAAO,KAAK,OAAO,YAAU,YAAU,QAAM,SAAQ,MAAM,IAAI,MAAM,oDAAoD;CAC5K;CACA,aAAqB,QAAmB,OAAmB;EACzD,MAAM,MAAI,KAAK,OAAO,UAAU,QAAQ,6BAA4B,EAAE;EACtE,KAAI,IAAI,QAAM,GAAE,QAAM,MAAM,cAAa,SAAQ;GAC/C,MAAM,OAAK,MAAM,SAAS,KAAK,CAAC,CAAC,gBAAgB,EAAE,GAAG;GACtD,IAAG,CAAC,MAAK,MAAM,IAAI,MAAM,6BAA6B;GACtD,0BAA0B,QAAO,OAAM,CAAC,GAAG,GAAE,KAAA,GAAU,KAAA,GAAU,YAAY,KAAK,SAAS,GAAE,EAAE,GAAE,KAAK,KAAK,SAAO,EAAE,CAAC;EACvH;CACF;CACA,cAAsB,GAAgB,GAAe;EACnD,KAAK,YAAY,CAAC;EAClB,IAAG,EAAE,YAAU,EAAE,WAAW,EAAE,WAAS,EAAE,UAAU,EAAE,QAAM,EAAE,OAAO,EAAE,MAAI,EAAE,UAAU,YAAY,SAAS,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAK,EAAE,MAAK,MAAM,IAAI,MAAM,qDAAqD;CACpN;AACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as backupFileProtection, r as restoreProfile } from "./backup-
|
|
1
|
+
import { n as backupFileProtection, r as restoreProfile } from "./backup-CnMN2oUK.js";
|
|
2
2
|
export { backupFileProtection, restoreProfile };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { vl as toBase64Url } from "./src-DDzSERyc.js";
|
|
2
2
|
import { c as fileStore } from "./idb-B5L8wuOm.js";
|
|
3
|
-
import { i as fromBase64Url, r as open$1 } from "./envelope-
|
|
4
|
-
import { t as CliError } from "./errors-
|
|
5
|
-
import { _ as str, n as bool } from "./apiKit-
|
|
6
|
-
import { d as profilePaths, n as RESTORED_MARK, u as profileExists } from "./profiles-
|
|
3
|
+
import { i as fromBase64Url, r as open$1 } from "./envelope-WjingYSc.js";
|
|
4
|
+
import { t as CliError } from "./errors-DO5z6h_l.js";
|
|
5
|
+
import { _ as str, n as bool } from "./apiKit-CA2x5GJF.js";
|
|
6
|
+
import { d as profilePaths, n as RESTORED_MARK, u as profileExists } from "./profiles-BHc9pEYg.js";
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
9
9
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -389,7 +389,7 @@ var BACKUP_METHODS = {
|
|
|
389
389
|
*/
|
|
390
390
|
async "profile.backup"(ctx, params) {
|
|
391
391
|
const path = resolve(str(params, "path", true));
|
|
392
|
-
if (existsSync(path)) throw new CliError("
|
|
392
|
+
if (existsSync(path)) throw new CliError("refused", `${path} exists: choose another name`);
|
|
393
393
|
const passphrase = str(params, "passphrase"), unsealed = bool(params, "noPassphrase");
|
|
394
394
|
if (!passphrase && !unsealed) throw new CliError("bad_request", "passphrase is required (or noPassphrase: true for a backup anyone can read)");
|
|
395
395
|
if (passphrase && unsealed) throw new CliError("bad_request", "Give a passphrase or noPassphrase, not both");
|
|
@@ -463,7 +463,7 @@ async "profile.backup"(ctx, params) {
|
|
|
463
463
|
try {
|
|
464
464
|
await link(partial, path);
|
|
465
465
|
} catch (error) {
|
|
466
|
-
throw error.code === "EEXIST" ? new CliError("
|
|
466
|
+
throw error.code === "EEXIST" ? new CliError("refused", `${path} exists: choose another name`) : error;
|
|
467
467
|
}
|
|
468
468
|
return {
|
|
469
469
|
path,
|
|
@@ -728,4 +728,4 @@ async function writeWhole(paths, payload) {
|
|
|
728
728
|
//#endregion
|
|
729
729
|
export { backupFileProtection as n, restoreProfile as r, BACKUP_METHODS as t };
|
|
730
730
|
|
|
731
|
-
//# sourceMappingURL=backup-
|
|
731
|
+
//# sourceMappingURL=backup-CnMN2oUK.js.map
|