@ghostlytools/cli 1.3.8 → 1.3.10
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/dist/assets/{api-Q6QZ0oIy.js → api-BCrwJ57L.js} +9 -9
- package/dist/assets/{api-Q6QZ0oIy.js.map → api-BCrwJ57L.js.map} +1 -1
- package/dist/assets/{api-CYG-sByy.js → api-Cp9GxjUO.js} +2 -2
- package/dist/assets/{apiKit-C9lLXQwt.js → apiKit-B4qhnLQT.js} +2 -2
- package/dist/assets/{apiKit-C9lLXQwt.js.map → apiKit-B4qhnLQT.js.map} +1 -1
- package/dist/assets/{apps-Bs8xpLGv.js → apps-CpxENMgl.js} +1 -1
- package/dist/assets/{apps-tcynvhlC.js → apps-DOH1P_S0.js} +2 -2
- package/dist/assets/{apps-tcynvhlC.js.map → apps-DOH1P_S0.js.map} +1 -1
- package/dist/assets/{arkade-BgmB0fUk.js → arkade-Cyf5MGxG.js} +2 -2
- package/dist/assets/{arkade-BgmB0fUk.js.map → arkade-Cyf5MGxG.js.map} +1 -1
- package/dist/assets/{backup-XtMACsQ0.js → backup-CTpFFQ-4.js} +4 -4
- package/dist/assets/{backup-XtMACsQ0.js.map → backup-CTpFFQ-4.js.map} +1 -1
- package/dist/assets/{backup-OlEHNkqQ.js → backup-CqM-0-2v.js} +1 -1
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/{buttons-mdl2Gol2.js → buttons-BtxwyM2l.js} +3 -3
- package/dist/assets/{buttons-mdl2Gol2.js.map → buttons-BtxwyM2l.js.map} +1 -1
- package/dist/assets/buttons-CJqNkG-Q.js +2 -0
- package/dist/assets/{envelope-DHf3mrHv.js → envelope-DW8TWKPD.js} +2 -2
- package/dist/assets/{envelope-DHf3mrHv.js.map → envelope-DW8TWKPD.js.map} +1 -1
- package/dist/assets/{groupFiles-CqrujNN1.js → groupFiles-uxa0U9_n.js} +3 -3
- package/dist/assets/{groupFiles-CqrujNN1.js.map → groupFiles-uxa0U9_n.js.map} +1 -1
- package/dist/assets/{host-Mz74S9cM.js → host-BbDWamRc.js} +112 -13
- package/dist/assets/host-BbDWamRc.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-BnqdXvmv.js → hyperdhtRelay-De5FT7Am.js} +2 -2
- package/dist/assets/{hyperdhtRelay-BnqdXvmv.js.map → hyperdhtRelay-De5FT7Am.js.map} +1 -1
- package/dist/assets/{irohWeb-C8iKFl8d.js → irohWeb-z5QDDWa-.js} +2 -2
- package/dist/assets/{irohWeb-C8iKFl8d.js.map → irohWeb-z5QDDWa-.js.map} +1 -1
- package/dist/assets/{main-BXwlJall.js → main-_q4Fgypp.js} +14 -14
- package/dist/assets/main-_q4Fgypp.js.map +1 -0
- package/dist/assets/{mainline-CbrPHUrZ.js → mainline-Cld-bKeC.js} +2 -2
- package/dist/assets/{mainline-CbrPHUrZ.js.map → mainline-Cld-bKeC.js.map} +1 -1
- package/dist/assets/{networkReason-CrrB031Y.js → networkReason-vUHo6A_C.js} +2 -2
- package/dist/assets/{networkReason-CrrB031Y.js.map → networkReason-vUHo6A_C.js.map} +1 -1
- package/dist/assets/{nwc-Cu5zwrE8.js → nwc-zXpmd2FN.js} +3 -3
- package/dist/assets/{nwc-Cu5zwrE8.js.map → nwc-zXpmd2FN.js.map} +1 -1
- package/dist/assets/{nwcProvider-BVVFfJM5.js → nwcProvider-D482SQ1a.js} +2 -2
- package/dist/assets/{nwcProvider-BVVFfJM5.js.map → nwcProvider-D482SQ1a.js.map} +1 -1
- package/dist/assets/{registry-CshC1EyV.js → registry-CxsDR3c6.js} +1 -1
- package/dist/assets/{registry-BKm8N_TI.js → registry-OVx2kY3d.js} +3 -3
- package/dist/assets/{registry-BKm8N_TI.js.map → registry-OVx2kY3d.js.map} +1 -1
- package/dist/assets/{resolve-DKGaPeMz.js → resolve-CPTAnu-9.js} +2 -2
- package/dist/assets/{resolve-DKGaPeMz.js.map → resolve-CPTAnu-9.js.map} +1 -1
- package/dist/assets/{server-CaDVxJKy.js → server-C-EkLCCt.js} +41 -24
- package/dist/assets/server-C-EkLCCt.js.map +1 -0
- package/dist/assets/{src-CVYir6mP.js → src-Dg1bvMNv.js} +29 -14
- package/dist/assets/src-Dg1bvMNv.js.map +1 -0
- package/dist/assets/{turnRefs-BeWsTX0o.js → turnRefs-WnggXm2T.js} +2 -2
- package/dist/assets/{turnRefs-BeWsTX0o.js.map → turnRefs-WnggXm2T.js.map} +1 -1
- package/dist/assets/{usdt-C5Q0BWHr.js → usdt-BY5OIIvG.js} +3 -3
- package/dist/assets/{usdt-C5Q0BWHr.js.map → usdt-BY5OIIvG.js.map} +1 -1
- package/dist/ghostly.mjs +1 -1
- package/package.json +1 -1
- package/dist/assets/buttons-BoGOYSHb.js +0 -2
- package/dist/assets/host-Mz74S9cM.js.map +0 -1
- package/dist/assets/main-BXwlJall.js.map +0 -1
- package/dist/assets/server-CaDVxJKy.js.map +0 -1
- package/dist/assets/src-CVYir6mP.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-B4qhnLQT.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-BCrwJ57L.js";
|
|
3
3
|
export { API_METHODS, SETTABLE, callApi, findChat, findGroup, groupSendRefused, hideStateSecrets, mentionsFor, redactSettings };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Gn as INVITE_TAKEN, Sl as isZ32Key } from "./src-
|
|
1
|
+
import { Gn as INVITE_TAKEN, Sl as isZ32Key } from "./src-Dg1bvMNv.js";
|
|
2
2
|
import { t as CliError } from "./errors-B-zMxbcd.js";
|
|
3
3
|
//#region src/apiKit.ts
|
|
4
4
|
var node = (ctx) => ctx.runtime.server.node;
|
|
@@ -208,4 +208,4 @@ function waitForState(ctx, check, ms, what) {
|
|
|
208
208
|
//#endregion
|
|
209
209
|
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 };
|
|
210
210
|
|
|
211
|
-
//# sourceMappingURL=apiKit-
|
|
211
|
+
//# sourceMappingURL=apiKit-B4qhnLQT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiKit-C9lLXQwt.js","names":[],"sources":["../../src/apiKit.ts"],"sourcesContent":["import { INVITE_TAKEN, isZ32Key } 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}\n/**\n * One of `values`; `fallback` when it was left out. Given empty (an unset variable in a script: `wallet remove \"\"`), it\n * is refused like any other wrong choice, never read as left out: that acted on the default (the Cashu wallet).\n */\nexport function oneOf<T extends string>(params: Params, name: string, values: readonly T[], fallback: T): T {\n const value = params[name] === \"\" ? \"\" : 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\nconst KEY_SHAPED = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;\n/**\n * A contact key as chats show it (`peer`): 52 characters of z-base-32, in its one canonical spelling. The same 52\n * characters with the last one respelled decode to the same bytes but are no key (WISP 02): refused here, so that\n * they never fall through to a chat's name, which its contact chooses.\n */\nexport const CONTACT_KEY = {\n test(ref: string): boolean {\n if (isZ32Key(ref)) return true;\n if (KEY_SHAPED.test(ref)) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} is not a contact key: a key has one spelling, and its last character is not this one`);\n return false;\n },\n};\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 the label this side gave\n * it; and only when no label matches, by a unique prefix of its id or the name the contact gave itself. The contact\n * controls both of those (a joiner's chat id is made from the inviter's code), so neither wins over a label, and a ref\n * that fits one chat's prefix and another's name is refused.\n */\nexport function findChat(links: readonly LinkView[], ref: string): LinkView {\n return chatBy(links, ref).link;\n}\n\n/** `findChat`, and whether only what the contact controls (`theirName`: a prefix of the id, its own name) found the chat. */\nfunction chatBy(links: readonly LinkView[], ref: string): { link: LinkView; theirName: boolean } {\n // \"\" (an unset variable in a script) is a prefix of every id: it took the only chat.\n if (!ref.trim()) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names no chat`);\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 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 found = byLabel.length ? byLabel : [...new Set([...links.filter((link) => link.id.startsWith(ref)), ...links.filter((link) => named(link.peerNick))])];\n if (found.length === 1) return { link: found[0], theirName: !byLabel.length };\n if (found.length > 1) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: found.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 its name, or (only when no name matches) by a unique prefix of its id: whoever created a group\n * chose its id, so a prefix never wins over a name. A group this profile is in wins over an invitation with the same\n * name or prefix (the one who invites chooses those); an invitation is still found by its id, and by name or prefix\n * where no group of this profile has it.\n */\nexport function findGroup(groups: readonly GroupView[], ref: string): GroupView {\n if (!ref.trim()) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names no group`);\n const exact = groups.find((group) => group.id === ref);\n if (exact) return exact;\n const lower = ref.toLowerCase();\n for (const among of [groups.filter((group) => !invitedOnly(group)), groups.filter(invitedOnly)]) {\n const byName = among.filter((group) => group.name.trim().toLowerCase() === lower);\n const found = byName.length ? byName : among.filter((group) => group.id.startsWith(ref));\n if (found.length === 1) return found[0];\n if (found.length > 1) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names more than one group: use its id`, { matches: found.map((group) => group.id) });\n }\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 what its contact chose (a prefix of its id, its own name) never takes what also\n * 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 (by what 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
|
+
{"version":3,"file":"apiKit-B4qhnLQT.js","names":[],"sources":["../../src/apiKit.ts"],"sourcesContent":["import { INVITE_TAKEN, isZ32Key } 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}\n/**\n * One of `values`; `fallback` when it was left out. Given empty (an unset variable in a script: `wallet remove \"\"`), it\n * is refused like any other wrong choice, never read as left out: that acted on the default (the Cashu wallet).\n */\nexport function oneOf<T extends string>(params: Params, name: string, values: readonly T[], fallback: T): T {\n const value = params[name] === \"\" ? \"\" : 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\nconst KEY_SHAPED = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;\n/**\n * A contact key as chats show it (`peer`): 52 characters of z-base-32, in its one canonical spelling. The same 52\n * characters with the last one respelled decode to the same bytes but are no key (WISP 02): refused here, so that\n * they never fall through to a chat's name, which its contact chooses.\n */\nexport const CONTACT_KEY = {\n test(ref: string): boolean {\n if (isZ32Key(ref)) return true;\n if (KEY_SHAPED.test(ref)) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} is not a contact key: a key has one spelling, and its last character is not this one`);\n return false;\n },\n};\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 the label this side gave\n * it; and only when no label matches, by a unique prefix of its id or the name the contact gave itself. The contact\n * controls both of those (a joiner's chat id is made from the inviter's code), so neither wins over a label, and a ref\n * that fits one chat's prefix and another's name is refused.\n */\nexport function findChat(links: readonly LinkView[], ref: string): LinkView {\n return chatBy(links, ref).link;\n}\n\n/** `findChat`, and whether only what the contact controls (`theirName`: a prefix of the id, its own name) found the chat. */\nfunction chatBy(links: readonly LinkView[], ref: string): { link: LinkView; theirName: boolean } {\n // \"\" (an unset variable in a script) is a prefix of every id: it took the only chat.\n if (!ref.trim()) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names no chat`);\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 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 found = byLabel.length ? byLabel : [...new Set([...links.filter((link) => link.id.startsWith(ref)), ...links.filter((link) => named(link.peerNick))])];\n if (found.length === 1) return { link: found[0], theirName: !byLabel.length };\n if (found.length > 1) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names more than one chat: use its id`, { matches: found.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 its name, or (only when no name matches) by a unique prefix of its id: whoever created a group\n * chose its id, so a prefix never wins over a name. A group this profile is in wins over an invitation with the same\n * name or prefix (the one who invites chooses those); an invitation is still found by its id, and by name or prefix\n * where no group of this profile has it.\n */\nexport function findGroup(groups: readonly GroupView[], ref: string): GroupView {\n if (!ref.trim()) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names no group`);\n const exact = groups.find((group) => group.id === ref);\n if (exact) return exact;\n const lower = ref.toLowerCase();\n for (const among of [groups.filter((group) => !invitedOnly(group)), groups.filter(invitedOnly)]) {\n const byName = among.filter((group) => group.name.trim().toLowerCase() === lower);\n const found = byName.length ? byName : among.filter((group) => group.id.startsWith(ref));\n if (found.length === 1) return found[0];\n if (found.length > 1) throw new CliError(\"bad_request\", `${JSON.stringify(ref)} names more than one group: use its id`, { matches: found.map((group) => group.id) });\n }\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 what its contact chose (a prefix of its id, its own name) never takes what also\n * 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 (by what 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,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-DOH1P_S0.js";
|
|
2
2
|
export { initApp, publishApp, revokeApp, signStore, verifyApp };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Co as canonicalJsonBytes, Mc as seedSigner, So as canonicalJson, Tl as toZ32, _ as assembleAppBundle, bo as signAppObject, c as readAppStore, d as APP_BUNDLE_LIMITS, do as APP_STATEMENT_LIMITS, fo as appFingerprint, mo as checkAppRevokeStatement, n as appStoreDecision, po as appRef, s as checkAppStoreIndex, t as appRevocationFor, u as signAppStore, uo as APP_PREFIXES, v as checkAppManifest, wl as toBase64Url, x as readAppBundle, xl as fromBase64Url, xo as signAppRevocation, y as isAppUrl, yo as readAppRevocations } from "./src-
|
|
1
|
+
import { Co as canonicalJsonBytes, Mc as seedSigner, So as canonicalJson, Tl as toZ32, _ as assembleAppBundle, bo as signAppObject, c as readAppStore, d as APP_BUNDLE_LIMITS, do as APP_STATEMENT_LIMITS, fo as appFingerprint, mo as checkAppRevokeStatement, n as appStoreDecision, po as appRef, s as checkAppStoreIndex, t as appRevocationFor, u as signAppStore, uo as APP_PREFIXES, v as checkAppManifest, wl as toBase64Url, x as readAppBundle, xl as fromBase64Url, xo as signAppRevocation, y as isAppUrl, yo as readAppRevocations } from "./src-Dg1bvMNv.js";
|
|
2
2
|
import { t as CliError } from "./errors-B-zMxbcd.js";
|
|
3
3
|
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -850,4 +850,4 @@ async function signStore(indexArg, keyArg, outArg, now = Math.floor(Date.now() /
|
|
|
850
850
|
//#endregion
|
|
851
851
|
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 };
|
|
852
852
|
|
|
853
|
-
//# sourceMappingURL=apps-
|
|
853
|
+
//# sourceMappingURL=apps-DOH1P_S0.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps-tcynvhlC.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 --prefix .. @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, install it in the folder above this one, where your editor\nstill finds it: \\`ghostly app publish\\` takes every file of this folder, so \\`node_modules\\` and \\`package.json\\` stay\nout of it.\n\n\\`\\`\\`sh\nnpm install --save-dev --prefix .. @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, appRevocationFor, 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 AppStoreReading, 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/**\n * The `ghostly-revoke.json` beside a bundle, as a client reads it beside each bundle URL: beside the URL (without its\n * query), or beside the file. None there, one that does not answer, or one a client would not read, revokes nothing.\n */\nasync function revocationsBeside(source: string, isUrl: boolean): Promise<{ file: string; revocations: SignedAppRevocation[] }> {\n let file: string;\n let bytes: Uint8Array;\n try {\n if (isUrl) {\n const at = new URL(source);\n at.search = \"\";\n at.hash = \"\";\n file = new URL(APP_REVOKE_FILE, at).href;\n bytes = await fetchBundle(file);\n } else {\n file = join(dirname(resolve(source)), APP_REVOKE_FILE);\n if (!existsSync(file)) return { file, revocations: [] };\n bytes = readBundleFile(file);\n }\n } catch { return { file: source, revocations: [] }; }\n const read = readAppRevocations(bytes);\n return { file, revocations: read.ok ? read.revocations : [] };\n}\n\n/**\n * A bundle from a file or an https URL, with every check of `app verify`; `url` when it came from one. A version its\n * publisher revoked in the `ghostly-revoke.json` beside it is refused: a client stops it, with no \"Run anyway\".\n */\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 const m = read.bundle.manifest;\n const { file, revocations } = await revocationsBeside(source, isUrl);\n const revocation = appRevocationFor({ ref: appRef(m.publisher, m.name), sequence: m.sequence, digest: read.bundle.digest }, revocations);\n if (revocation) {\n throw refused(`${source} is a version its publisher revoked in ${file}: a client stops it`, { reason: \"revoked\", ...(revocation.statement.reason !== undefined && { detail: revocation.statement.reason }) }, { file });\n }\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 // Nor one the folder's own revocations already stop: every client that reads them would refuse it, for good.\n const ref = appRef(key.key, checked.manifest.name);\n const revocations = existsSync(join(dir, APP_REVOKE_FILE)) ? readAppRevocations(new Uint8Array(readFileSync(join(dir, APP_REVOKE_FILE)))) : null;\n const held = revocations?.ok ? revocations.revocations : [];\n if (appRevocationFor({ ref, sequence, digest: read.bundle.digest }, held)) {\n const clear = Math.max(sequence, ...held.map((r) => r.statement.app === ref && \"upTo\" in r.statement ? r.statement.upTo : 0)) + 1;\n throw refused(`${join(dir, APP_REVOKE_FILE)} already revokes version ${sequence} of ${ref}, and every client that reads it would stop it: give --sequence ${clear}`, { reason: \"revoked\" }, { sequence: clear });\n }\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; force?: boolean }): 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 // Clients keep a revocation for good: past the newest version (a typo, 30 for 3), it stops versions not made yet.\n if (what.upTo !== undefined && what.upTo > manifest.sequence && !what.force) {\n throw new CliError(\"usage\", `--up-to ${what.upTo} is past ${bundlePath}'s version, ${manifest.sequence}: it would also stop versions ${manifest.sequence + 1} to ${what.upTo} before they exist, for good. Give --up-to ${manifest.sequence}, or --force if versions up to ${what.upTo} were published from elsewhere`);\n }\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 has a higher `sequence`, or the same one with other content, or is signed by another key (another\n * store is another `--out`). The key is made only when no signed index is there.\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 // The index this one follows, when the folder holds a signed one: a store is its key, so only that key signs over it.\n const indexOut = join(outDir, STORE_INDEX_FILE), sigOut = join(outDir, STORE_SIG_FILE);\n let held: AppStoreReading | null = null;\n if (existsSync(indexOut) && existsSync(sigOut)) {\n const read = readAppStore(new Uint8Array(readFileSync(indexOut)), new Uint8Array(readFileSync(sigOut)), now);\n if (read.ok) held = read.store;\n }\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 if (held) throw new CliError(\"not_found\", `No key file ${keyPath}: ${indexOut} is signed by ${appFingerprint(held.index.key)}, and only that key signs its next index`);\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 if (held && held.index.key !== key.key) {\n throw refused(`${indexOut} is signed by another store key (${appFingerprint(held.index.key)}) than ${keyPath}'s (${appFingerprint(key.key)}): a store is its key; sign with that key, or give another --out for another store`, { reason: \"store-key\" }, { held: held.index.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 let previous: number | null = null;\n if (held) {\n previous = held.index.sequence;\n const decision = appStoreDecision(\n { key: key.key, sequence: held.index.sequence, digest: held.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.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.index.sequence });\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-DOH1P_S0.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 --prefix .. @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, install it in the folder above this one, where your editor\nstill finds it: \\`ghostly app publish\\` takes every file of this folder, so \\`node_modules\\` and \\`package.json\\` stay\nout of it.\n\n\\`\\`\\`sh\nnpm install --save-dev --prefix .. @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, appRevocationFor, 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 AppStoreReading, 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/**\n * The `ghostly-revoke.json` beside a bundle, as a client reads it beside each bundle URL: beside the URL (without its\n * query), or beside the file. None there, one that does not answer, or one a client would not read, revokes nothing.\n */\nasync function revocationsBeside(source: string, isUrl: boolean): Promise<{ file: string; revocations: SignedAppRevocation[] }> {\n let file: string;\n let bytes: Uint8Array;\n try {\n if (isUrl) {\n const at = new URL(source);\n at.search = \"\";\n at.hash = \"\";\n file = new URL(APP_REVOKE_FILE, at).href;\n bytes = await fetchBundle(file);\n } else {\n file = join(dirname(resolve(source)), APP_REVOKE_FILE);\n if (!existsSync(file)) return { file, revocations: [] };\n bytes = readBundleFile(file);\n }\n } catch { return { file: source, revocations: [] }; }\n const read = readAppRevocations(bytes);\n return { file, revocations: read.ok ? read.revocations : [] };\n}\n\n/**\n * A bundle from a file or an https URL, with every check of `app verify`; `url` when it came from one. A version its\n * publisher revoked in the `ghostly-revoke.json` beside it is refused: a client stops it, with no \"Run anyway\".\n */\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 const m = read.bundle.manifest;\n const { file, revocations } = await revocationsBeside(source, isUrl);\n const revocation = appRevocationFor({ ref: appRef(m.publisher, m.name), sequence: m.sequence, digest: read.bundle.digest }, revocations);\n if (revocation) {\n throw refused(`${source} is a version its publisher revoked in ${file}: a client stops it`, { reason: \"revoked\", ...(revocation.statement.reason !== undefined && { detail: revocation.statement.reason }) }, { file });\n }\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 // Nor one the folder's own revocations already stop: every client that reads them would refuse it, for good.\n const ref = appRef(key.key, checked.manifest.name);\n const revocations = existsSync(join(dir, APP_REVOKE_FILE)) ? readAppRevocations(new Uint8Array(readFileSync(join(dir, APP_REVOKE_FILE)))) : null;\n const held = revocations?.ok ? revocations.revocations : [];\n if (appRevocationFor({ ref, sequence, digest: read.bundle.digest }, held)) {\n const clear = Math.max(sequence, ...held.map((r) => r.statement.app === ref && \"upTo\" in r.statement ? r.statement.upTo : 0)) + 1;\n throw refused(`${join(dir, APP_REVOKE_FILE)} already revokes version ${sequence} of ${ref}, and every client that reads it would stop it: give --sequence ${clear}`, { reason: \"revoked\" }, { sequence: clear });\n }\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; force?: boolean }): 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 // Clients keep a revocation for good: past the newest version (a typo, 30 for 3), it stops versions not made yet.\n if (what.upTo !== undefined && what.upTo > manifest.sequence && !what.force) {\n throw new CliError(\"usage\", `--up-to ${what.upTo} is past ${bundlePath}'s version, ${manifest.sequence}: it would also stop versions ${manifest.sequence + 1} to ${what.upTo} before they exist, for good. Give --up-to ${manifest.sequence}, or --force if versions up to ${what.upTo} were published from elsewhere`);\n }\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 has a higher `sequence`, or the same one with other content, or is signed by another key (another\n * store is another `--out`). The key is made only when no signed index is there.\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 // The index this one follows, when the folder holds a signed one: a store is its key, so only that key signs over it.\n const indexOut = join(outDir, STORE_INDEX_FILE), sigOut = join(outDir, STORE_SIG_FILE);\n let held: AppStoreReading | null = null;\n if (existsSync(indexOut) && existsSync(sigOut)) {\n const read = readAppStore(new Uint8Array(readFileSync(indexOut)), new Uint8Array(readFileSync(sigOut)), now);\n if (read.ok) held = read.store;\n }\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 if (held) throw new CliError(\"not_found\", `No key file ${keyPath}: ${indexOut} is signed by ${appFingerprint(held.index.key)}, and only that key signs its next index`);\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 if (held && held.index.key !== key.key) {\n throw refused(`${indexOut} is signed by another store key (${appFingerprint(held.index.key)}) than ${keyPath}'s (${appFingerprint(key.key)}): a store is its key; sign with that key, or give another --out for another store`, { reason: \"store-key\" }, { held: held.index.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 let previous: number | null = null;\n if (held) {\n previous = held.index.sequence;\n const decision = appStoreDecision(\n { key: key.key, sequence: held.index.sequence, digest: held.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.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.index.sequence });\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,4 +1,4 @@
|
|
|
1
|
-
import { _n as validatePaymentTarget, hn as assertWholeSats, pn as arkAddressPrefix } from "./src-
|
|
1
|
+
import { _n as validatePaymentTarget, hn as assertWholeSats, pn as arkAddressPrefix } from "./src-Dg1bvMNv.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-Cyf5MGxG.js.map
|