@hoardodile/host-web 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -20,21 +20,32 @@ export { ReadFileRange } from '@hoardodile/sdk-types';
20
20
  * except through `close`/`decide`, which the two hosts wire to their
21
21
  * own decision path.
22
22
  */
23
+ /**
24
+ * One item of a batch consent question: a single download's URL and
25
+ * vault destination, exactly as the dialog lists it.
26
+ */
27
+ type DownloadConsentItem = {
28
+ readonly url: string;
29
+ readonly dest: string;
30
+ readonly sizeBytes?: number;
31
+ readonly reason?: string;
32
+ };
23
33
  /**
24
34
  * A queued consent question: the ticket shape. Structurally identical to
25
35
  * the server's `pluginDownloadRequested` SSE event minus its `type`
26
36
  * discriminator (and to `pluginAsset.listPending` rows) — declared here
27
37
  * instead of importing `@hoardodile/schemas` so the host-core package
28
38
  * stays dependency-lean.
39
+ *
40
+ * One entry = one dialog listing every item (a single download is an
41
+ * items array of one; a batched plugin call is one entry for the whole
42
+ * batch).
29
43
  */
30
44
  type DownloadConsentEntry = {
31
45
  readonly ticketId: string;
32
46
  readonly pluginId: string;
33
47
  readonly pluginName: string;
34
- readonly url: string;
35
- readonly dest: string;
36
- readonly sizeBytes?: number;
37
- readonly reason?: string;
48
+ readonly items: readonly DownloadConsentItem[];
38
49
  };
39
50
  type State = {
40
51
  readonly queue: readonly DownloadConsentEntry[];
@@ -119,12 +130,17 @@ declare const requestSchemas: {
119
130
  danmaku: "danmaku";
120
131
  }>;
121
132
  }, z.core.$strip>;
122
- download: z.ZodObject<{
133
+ download: z.ZodUnion<readonly [z.ZodObject<{
123
134
  url: z.ZodString;
124
135
  dest: z.ZodString;
125
136
  sha256: z.ZodOptional<z.ZodString>;
126
137
  reason: z.ZodOptional<z.ZodString>;
127
- }, z.core.$strip>;
138
+ }, z.core.$strip>, z.ZodArray<z.ZodObject<{
139
+ url: z.ZodString;
140
+ dest: z.ZodString;
141
+ sha256: z.ZodOptional<z.ZodString>;
142
+ reason: z.ZodOptional<z.ZodString>;
143
+ }, z.core.$strip>>]>;
128
144
  deleteAsset: z.ZodObject<{
129
145
  path: z.ZodString;
130
146
  }, z.core.$strip>;
@@ -266,7 +282,7 @@ type MockHostOptions = {
266
282
  * request/result vocabulary.
267
283
  */
268
284
  type PluginAssetVaultMock = {
269
- readonly download: (request: PluginDownloadRequest) => Promise<PluginDownloadResult>;
285
+ readonly download: (request: PluginDownloadRequest | readonly PluginDownloadRequest[]) => Promise<PluginDownloadResult | readonly PluginDownloadResult[]>;
270
286
  readonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>;
271
287
  };
272
288
  type MockHost = {
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { PLUGIN_ASSET_BATCH_MAX_ITEMS } from '@hoardodile/sdk-types/plugin-asset-limits';
1
2
  import { anchorData as anchorData$1 } from '@hoardodile/sdk-types/schema';
2
3
  import { pluginMethods, PROTOCOL_VERSION, invalidatePushKeys, hostPushKeys } from '@hoardodile/sdk-web';
3
4
  import { z } from 'zod';
@@ -60,6 +61,12 @@ function resetDownloadConsent() {
60
61
  setState({ queue: [] });
61
62
  }
62
63
  var anchorData = anchorData$1;
64
+ var downloadItem = z.object({
65
+ url: z.string().min(1).max(2048),
66
+ dest: z.string().min(1).max(256),
67
+ sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
68
+ reason: z.string().max(200).optional()
69
+ });
63
70
  var requestSchemas = {
64
71
  [pluginMethods.readFile]: z.object({
65
72
  path: z.string().min(1),
@@ -96,13 +103,13 @@ var requestSchemas = {
96
103
  target: z.enum(["resource", "resources", "messages", "danmaku"])
97
104
  }),
98
105
  // The URL is length-capped but not URI-validated here: the host parses
99
- // it server-side and answers with a machine-readable POLICY error.
100
- [pluginMethods.download]: z.object({
101
- url: z.string().min(1).max(2048),
102
- dest: z.string().min(1).max(256),
103
- sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
104
- reason: z.string().max(200).optional()
105
- }),
106
+ // it server-side and answers with a machine-readable POLICY error. A
107
+ // single request or a batch (one consent dialog listing every item,
108
+ // all-or-nothing) — capped so a burst cannot stack unbounded tickets.
109
+ [pluginMethods.download]: z.union([
110
+ downloadItem,
111
+ z.array(downloadItem).min(1).max(PLUGIN_ASSET_BATCH_MAX_ITEMS)
112
+ ]),
106
113
  [pluginMethods.deleteAsset]: z.object({
107
114
  path: z.string().min(1).max(256)
108
115
  }),
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/consent/consent-store.ts","../src/host-core/request-schemas.ts","../src/host-core/router.ts","../src/mock/file-backends.ts","../src/mock/stores.ts","../src/mock/host.ts"],"names":["anchorDataSchema","pluginMethods"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The consent-dialog queue for plugin asset downloads — the shared\n * implementation both host apps mount:\n *\n * - the **app** feeds it from SSE (`pluginDownloadRequested` /\n * `pluginDownloadResolved`) and drives decisions through the server\n * (`pluginAsset.decide`); the resolved broadcast closes entries here;\n * - the **workbench** has no server: its asset engine enqueues via\n * {@link request} and the dialog answers via {@link decide} — the same\n * dialog component, one queue per page, FIFO, one dialog at a time.\n *\n * State-only by design: decisions never mutate the queue directly\n * except through `close`/`decide`, which the two hosts wire to their\n * own decision path.\n */\n\n/**\n * A queued consent question: the ticket shape. Structurally identical to\n * the server's `pluginDownloadRequested` SSE event minus its `type`\n * discriminator (and to `pluginAsset.listPending` rows) — declared here\n * instead of importing `@hoardodile/schemas` so the host-core package\n * stays dependency-lean.\n */\nexport type DownloadConsentEntry = {\n\treadonly ticketId: string\n\treadonly pluginId: string\n\treadonly pluginName: string\n\treadonly url: string\n\treadonly dest: string\n\treadonly sizeBytes?: number\n\treadonly reason?: string\n}\n\ntype State = {\n\treadonly queue: readonly DownloadConsentEntry[]\n}\n\nlet state: State = { queue: [] }\nconst listeners = new Set<() => void>()\n\n/** Engine side: tickets awaiting a local decision (workbench path). */\nconst pending = new Map<\n\tstring,\n\t{ readonly resolve: (approved: boolean) => void }\n>()\n\nfunction setState(next: State): void {\n\tstate = next\n\tfor (const listener of listeners) {\n\t\tlistener()\n\t}\n}\n\nexport function subscribeDownloadConsent(listener: () => void): () => void {\n\tlisteners.add(listener)\n\treturn () => {\n\t\tlisteners.delete(listener)\n\t}\n}\n\nexport function getDownloadConsentSnapshot(): State {\n\treturn state\n}\n\n/** A new ticket arrived (SSE) or was requested locally: append if absent. */\nexport function enqueueDownloadConsent(entry: DownloadConsentEntry): void {\n\tfor (const existing of state.queue) {\n\t\tif (existing.ticketId === entry.ticketId) return\n\t}\n\tsetState({ queue: [...state.queue, entry] })\n}\n\n/** A ticket resolved (decide/timeout/dispose): drop it in every consumer. */\nexport function closeDownloadConsent(ticketId: string): void {\n\tconst queue = state.queue.filter((entry) => entry.ticketId !== ticketId)\n\tif (queue.length === state.queue.length) return\n\tsetState({ queue })\n}\n\n/**\n * Replace the queue from a pending list (app: `listPending` after an SSE\n * reconnect; workbench: same API on restart).\n */\nexport function rehydrateDownloadConsent(\n\tentries: readonly DownloadConsentEntry[],\n): void {\n\tconst byTicket = new Map<string, DownloadConsentEntry>()\n\tfor (const entry of entries) byTicket.set(entry.ticketId, entry)\n\tsetState({ queue: [...byTicket.values()] })\n}\n\n/**\n * Enqueue a ticket and wait for its decision (workbench engine path).\n * The dialog's answer arrives through {@link decideDownloadConsent}.\n */\nexport function requestDownloadConsent(\n\tentry: DownloadConsentEntry,\n): Promise<boolean> {\n\tif (pending.has(entry.ticketId)) {\n\t\treturn Promise.resolve(false)\n\t}\n\tenqueueDownloadConsent(entry)\n\treturn new Promise<boolean>((resolve) => {\n\t\tpending.set(entry.ticketId, { resolve })\n\t})\n}\n\n/**\n * Answer a ticket from the dialog. Closes the entry and resolves any\n * awaiting engine call. Idempotent; unknown tickets are a no-op.\n */\nexport function decideDownloadConsent(\n\tticketId: string,\n\tapproved: boolean,\n): void {\n\tconst waiter = pending.get(ticketId)\n\tif (waiter !== undefined) {\n\t\tpending.delete(ticketId)\n\t\twaiter.resolve(approved)\n\t}\n\tcloseDownloadConsent(ticketId)\n}\n\n/** Test-only: reset the singleton between tests. */\nexport function resetDownloadConsent(): void {\n\tfor (const waiter of pending.values()) waiter.resolve(false)\n\tpending.clear()\n\tsetState({ queue: [] })\n}\n","import { anchorData as anchorDataSchema } from \"@hoardodile/sdk-types/schema\"\nimport type { PluginRequests } from \"@hoardodile/sdk-web\"\nimport { pluginMethods } from \"@hoardodile/sdk-web\"\nimport { z } from \"zod\"\n\n/**\n * Wire-level param schemas for every request method, shared by the real\n * host (apps/web) and the offline mock host. Both hosts validate\n * identical shapes, so a plugin that passes validation in the mock is\n * guaranteed to pass in production.\n */\n\n/**\n * Wire shape of a message/danmaku anchor: the plugin location payload\n * only. The resource id is host state — the host stamps its own binding\n * into the anchor before it reaches the server.\n */\nexport const anchorData = anchorDataSchema\n\nexport const requestSchemas = {\n\t[pluginMethods.readFile]: z.object({\n\t\tpath: z.string().min(1),\n\t\trange: z\n\t\t\t.object({\n\t\t\t\tstart: z.number().int().nonnegative().optional(),\n\t\t\t\tend: z.number().int().nonnegative().optional(),\n\t\t\t})\n\t\t\t.optional(),\n\t}),\n\t[pluginMethods.listFiles]: undefined,\n\t[pluginMethods.listMessages]: undefined,\n\t[pluginMethods.createMessage]: z.object({\n\t\tbody: z.string().min(1),\n\t\tanchor: anchorData.optional(),\n\t}),\n\t[pluginMethods.listDanmaku]: z.object({\n\t\t// Filter keys are plugin-defined vocabulary; matching is generic\n\t\t// strict equality against the anchor's data.\n\t\tfilter: z\n\t\t\t.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))\n\t\t\t.optional(),\n\t}),\n\t[pluginMethods.createDanmaku]: z.object({\n\t\ttext: z.string().min(1),\n\t\tanchor: anchorData,\n\t\tmode: z.string().optional(),\n\t}),\n\t[pluginMethods.setPref]: z.object({\n\t\tkey: z.string().min(1),\n\t\tvalue: z.string(),\n\t}),\n\t[pluginMethods.setCache]: z.object({\n\t\tkey: z.string().min(1),\n\t\tvalue: z.string(),\n\t}),\n\t[pluginMethods.invalidate]: z.object({\n\t\ttarget: z.enum([\"resource\", \"resources\", \"messages\", \"danmaku\"]),\n\t}),\n\t// The URL is length-capped but not URI-validated here: the host parses\n\t// it server-side and answers with a machine-readable POLICY error.\n\t[pluginMethods.download]: z.object({\n\t\turl: z.string().min(1).max(2048),\n\t\tdest: z.string().min(1).max(256),\n\t\tsha256: z\n\t\t\t.string()\n\t\t\t.regex(/^[0-9a-f]{64}$/)\n\t\t\t.optional(),\n\t\treason: z.string().max(200).optional(),\n\t}),\n\t[pluginMethods.deleteAsset]: z.object({\n\t\tpath: z.string().min(1).max(256),\n\t}),\n\t[pluginMethods.logInfo]: z.object({\n\t\tmessage: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t}),\n\t[pluginMethods.logWarn]: z.object({\n\t\tmessage: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t}),\n\t[pluginMethods.logError]: z.object({\n\t\tmessage: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t}),\n} satisfies Record<keyof PluginRequests, z.ZodTypeAny | undefined>\n","import {\n\ttype HostResponse,\n\ttype PluginMessage,\n\tPROTOCOL_VERSION,\n} from \"@hoardodile/sdk-web\"\nimport type { z } from \"zod\"\n\n/**\n * The shared host-side protocol core: message demux, method routing,\n * per-method param validation, stale-request scoping and the response\n * envelope. Both the real host (apps/web) and the offline mock host\n * assemble on this module, so routing and validation never drift.\n */\n\n/** What a registered iframe (or mock window) is bound to. */\nexport type HostBinding = {\n\treadonly pluginId: string\n\treadonly resId: string\n}\n\nexport type HostRouterDeps = {\n\t/**\n\t * Authenticate an inbound message event: narrow it to a trusted source\n\t * + binding, or return `undefined` to drop the message. The real host\n\t * validates origin/source against its iframe registry; the mock\n\t * validates against its own registered windows.\n\t */\n\treadonly resolveSource: (\n\t\tevent: MessageEvent,\n\t) => { readonly source: unknown; readonly record: HostBinding } | undefined\n\t/** Send a response back to the source (the layer's postMessage exit). */\n\treadonly respond: (source: unknown, response: HostResponse) => void\n\t/** Record a subscription key for the source. */\n\treadonly subscribe: (source: unknown, key: string) => void\n}\n\nexport type HostHandlerContext = {\n\treadonly source: unknown\n\t/**\n\t * The resource this request is scoped to: the source's current\n\t * binding (\"\" only for never-bound sources). Requests stamped by the\n\t * SDK with a different resource are dropped as stale before they\n\t * reach a handler.\n\t */\n\treadonly resId: string\n\treadonly pluginId: string\n}\n\nexport type HostHandlerEntry = {\n\treadonly method: string\n\t/** Param schema; validated in the router before the handler runs. */\n\treadonly schema: z.ZodTypeAny | undefined\n\treadonly handler: (\n\t\tctx: HostHandlerContext,\n\t\tparams: unknown,\n\t) => Promise<unknown>\n}\n\nexport function defineHandler<TReturn>(\n\tmethod: string,\n\thandler: (ctx: HostHandlerContext) => Promise<TReturn> | TReturn,\n): HostHandlerEntry\n\nexport function defineHandler<TSchema extends z.ZodTypeAny, TReturn>(\n\tmethod: string,\n\tschema: TSchema,\n\thandler: (\n\t\tctx: HostHandlerContext,\n\t\tparams: z.infer<TSchema>,\n\t) => Promise<TReturn> | TReturn,\n): HostHandlerEntry\n\nexport function defineHandler(\n\tmethod: string,\n\tschemaOrHandler:\n\t\t| z.ZodTypeAny\n\t\t| ((ctx: HostHandlerContext) => Promise<unknown> | unknown),\n\tmaybeHandler?: (\n\t\tctx: HostHandlerContext,\n\t\tparams: unknown,\n\t) => Promise<unknown> | unknown,\n): HostHandlerEntry {\n\tif (typeof schemaOrHandler === \"function\") {\n\t\treturn {\n\t\t\tmethod,\n\t\t\tschema: undefined,\n\t\t\thandler: async (ctx) => schemaOrHandler(ctx),\n\t\t}\n\t}\n\tif (maybeHandler === undefined) {\n\t\tthrow new Error(`defineHandler(\"${method}\") called without a handler`)\n\t}\n\treturn {\n\t\tmethod,\n\t\tschema: schemaOrHandler,\n\t\thandler: async (ctx, params) => maybeHandler(ctx, params),\n\t}\n}\n\n/**\n * Create the per-window message handler: authenticates the event,\n * routes requests by method, validates params, drops stale scopes and\n * wraps every outcome in the response envelope.\n */\nexport function createHostRouter(\n\thandlers: readonly HostHandlerEntry[],\n\tdeps: HostRouterDeps,\n): (event: MessageEvent) => void {\n\tconst registry = new Map<string, HostHandlerEntry[\"handler\"]>()\n\tconst schemas = new Map<string, z.ZodTypeAny>()\n\tfor (const entry of handlers) {\n\t\tif (registry.has(entry.method)) {\n\t\t\tthrow new Error(`Duplicate handler method: ${entry.method}`)\n\t\t}\n\t\tregistry.set(entry.method, entry.handler)\n\t\tif (entry.schema !== undefined) schemas.set(entry.method, entry.schema)\n\t}\n\n\t// Warn once per source so a mismatched plugin build does not spam the\n\t// console on every message, while still surfacing the problem loudly.\n\tconst warnedSources = new WeakSet<object>()\n\n\treturn function handleMessage(event: MessageEvent) {\n\t\tconst resolved = deps.resolveSource(event)\n\t\tif (resolved === undefined) return\n\t\tconst { source, record } = resolved\n\n\t\tconst msg = event.data as PluginMessage\n\t\tif (msg == null || typeof msg !== \"object\" || msg.type === undefined) {\n\t\t\treturn\n\t\t}\n\n\t\t// Protocol handshake: every SDK-build plugin stamps its messages\n\t\t// with the protocol version it was built against. A mismatch (or a\n\t\t// missing stamp — an old plugin build) means the wire contract may\n\t\t// have drifted; the message still routes so old plugins keep\n\t\t// working, but the developer gets a loud warning.\n\t\tconst proto = (msg as { proto?: unknown }).proto\n\t\tif (proto !== PROTOCOL_VERSION && !warnedSources.has(source as object)) {\n\t\t\twarnedSources.add(source as object)\n\t\t\tif (proto === undefined) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[host-web] plugin iframe (${record.pluginId}) did not stamp its protocol version — it was built against an older SDK. Assuming PROTOCOL_VERSION ${PROTOCOL_VERSION}; rebuild the plugin to silence this warning.`,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[host-web] plugin iframe (${record.pluginId}) speaks protocol version ${String(proto)} but this host speaks ${PROTOCOL_VERSION} — the plugin was built against a different SDK version and may misbehave.`,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tif (msg.type === \"subscribe\") {\n\t\t\tdeps.subscribe(source, msg.key)\n\t\t\treturn\n\t\t}\n\n\t\tif (msg.type !== \"request\") return\n\n\t\tfunction respond(response: HostResponse): void {\n\t\t\tdeps.respond(source, response)\n\t\t}\n\n\t\t// The SDK stamps each request with the resource it was issued for\n\t\t// (PluginRequest.resId). A stamp that no longer matches the\n\t\t// binding marks the request as stale — the tree that issued it is\n\t\t// gone (e.g. an unmount flush racing a rebind) — so it is dropped\n\t\t// silently instead of leaking into the wrong resource. Unstamped\n\t\t// requests (older plugin builds) use the current binding, which\n\t\t// outlives release, so late flushes after a close still land.\n\t\tif (\n\t\t\ttypeof msg.resId === \"string\" &&\n\t\t\tmsg.resId !== \"\" &&\n\t\t\tmsg.resId !== record.resId\n\t\t) {\n\t\t\trespond({ type: \"response\", id: msg.id, ok: true })\n\t\t\treturn\n\t\t}\n\n\t\tconst ctx: HostHandlerContext = {\n\t\t\tsource,\n\t\t\tresId: record.resId,\n\t\t\tpluginId: record.pluginId,\n\t\t}\n\n\t\tconst handler = registry.get(msg.method)\n\t\tif (handler === undefined) {\n\t\t\trespond({\n\t\t\t\ttype: \"response\",\n\t\t\t\tid: msg.id,\n\t\t\t\tok: false,\n\t\t\t\terror: `Unknown method: ${msg.method}`,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tlet params: unknown\n\t\tconst schema = schemas.get(msg.method)\n\t\tif (schema !== undefined) {\n\t\t\tconst parsed = schema.safeParse(msg.params)\n\t\t\tif (!parsed.success) {\n\t\t\t\trespond({\n\t\t\t\t\ttype: \"response\",\n\t\t\t\t\tid: msg.id,\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: `Invalid params for ${msg.method}: ${parsed.error.message}`,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tparams = parsed.data\n\t\t} else {\n\t\t\tparams = msg.params\n\t\t}\n\n\t\thandler(ctx, params)\n\t\t\t.then((data) => {\n\t\t\t\trespond({ type: \"response\", id: msg.id, ok: true, data })\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconst errorName =\n\t\t\t\t\terr instanceof Error && err.name !== \"Error\" ? err.name : undefined\n\t\t\t\trespond({\n\t\t\t\t\ttype: \"response\",\n\t\t\t\t\tid: msg.id,\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t// Machine-readable plugin error code (`DENIED`/…)\n\t\t\t\t\t// travels as `errorCode`; `errorName` is the legacy\n\t\t\t\t\t// alias (same value) so older SDK builds keep working.\n\t\t\t\t\terrorCode: errorName,\n\t\t\t\t\terrorName,\n\t\t\t\t})\n\t\t\t})\n\t}\n}\n","import type { ReadFileRange } from \"@hoardodile/sdk-types\"\n\n/** Shared read-range contract; defined once in `@hoardodile/sdk-types`. */\nexport type { ReadFileRange } from \"@hoardodile/sdk-types\"\n\n/**\n * File backends for the offline mock host: the plugin's `listFiles` and\n * `readFile` requests resolve against this interface, so a mock can be\n * pointed at an in-memory map, a real directory (Node), or a read-only\n * HTTP mount (workbench) without changing the host.\n */\nexport type MockFileBackend = {\n\treadonly listFiles: (resId: string) => Promise<readonly string[]>\n\treadonly readFile: (\n\t\tresId: string,\n\t\tpath: string,\n\t\trange?: ReadFileRange,\n\t) => Promise<ArrayBuffer>\n\treadonly statFile: (\n\t\tresId: string,\n\t\tpath: string,\n\t) => Promise<{ readonly sizeBytes: number } | undefined>\n\t/**\n\t * The rows the plugin's own `listFiles` hook produced, when the\n\t * backend can obtain them (the workbench reads them from a sandboxed\n\t * hook snapshot). In production the host answers `listFiles` with\n\t * exactly these plugin-shaped entries; returning `undefined` falls\n\t * back to generic `{filename, ext, sizeBytes}` rows.\n\t */\n\treadonly listFileEntries?: (\n\t\tresId: string,\n\t) => Promise<readonly unknown[] | undefined>\n}\n\n/** In-memory file map backend for unit tests. `resId` is ignored. */\nexport function createInMemoryFileBackend(\n\tfiles: Readonly<Record<string, string | Uint8Array>> = {},\n): MockFileBackend {\n\treturn {\n\t\tasync listFiles() {\n\t\t\treturn Object.keys(files)\n\t\t},\n\t\tasync readFile(_resId, path, range) {\n\t\t\tconst content = files[path]\n\t\t\tif (content === undefined) {\n\t\t\t\tthrow new Error(`mock file backend has no entry ${path}`)\n\t\t\t}\n\t\t\tconst bytes =\n\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t? new TextEncoder().encode(content)\n\t\t\t\t\t: content\n\t\t\t// Mirrors host semantics: the range is clamped to the content\n\t\t\t// size; a start at or past the end resolves to an empty result.\n\t\t\tif (range === undefined) return bytes.slice().buffer\n\t\t\tconst start = Math.max(0, range.start ?? 0)\n\t\t\tconst end = Math.min(range.end ?? bytes.length, bytes.length)\n\t\t\treturn bytes.slice(start, end).buffer\n\t\t},\n\t\tasync statFile(_resId, path) {\n\t\t\tconst content = files[path]\n\t\t\tif (content === undefined) return undefined\n\t\t\tconst bytes =\n\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t? new TextEncoder().encode(content)\n\t\t\t\t\t: content\n\t\t\treturn { sizeBytes: bytes.byteLength }\n\t\t},\n\t}\n}\n","import type { Danmaku, Message } from \"@hoardodile/sdk-types\"\n\n/**\n * In-memory stores backing the offline mock host's message and danmaku\n * handlers. Rows are shaped like the server's responses so plugin UI\n * tests exercise the real consumption paths.\n */\n\nlet nextId = 1\n\nfunction generateId(prefix: string): string {\n\treturn `${prefix}-${nextId++}`\n}\n\nexport type MockMessageStore = {\n\treadonly list: (resId: string) => readonly Message[]\n\treadonly create: (\n\t\tresId: string,\n\t\tinput: { body: string; anchor?: unknown },\n\t) => Message\n}\n\n/**\n * `seed` pre-fills the store with rows the plugin should already see —\n * the workbench passes the resource's real comments so the iframe opens\n * with the same content the app would show.\n */\nexport function createMockMessageStore(\n\tseed: readonly Message[] = [],\n): MockMessageStore {\n\tconst rows: Message[] = [...seed]\n\treturn {\n\t\tlist(resId) {\n\t\t\treturn rows.filter((m) => m.resIds.includes(resId))\n\t\t},\n\t\tcreate(resId, input) {\n\t\t\tconst message: Message = {\n\t\t\t\tid: generateId(\"msg\"),\n\t\t\t\tbody: input.body,\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t\tcharIds: [],\n\t\t\t\tresIds: [resId],\n\t\t\t\tlikeCount: 0,\n\t\t\t\tdislikeCount: 0,\n\t\t\t\treplyCount: 0,\n\t\t\t\tanchor: input.anchor as Message[\"anchor\"],\n\t\t\t}\n\t\t\trows.push(message)\n\t\t\treturn message\n\t\t},\n\t}\n}\n\nexport type MockDanmakuStore = {\n\treadonly list: (resId: string) => readonly Danmaku[]\n\treadonly create: (\n\t\tresId: string,\n\t\tinput: { text: string; anchor: unknown; mode?: string },\n\t) => Danmaku\n}\n\n/** See {@link createMockMessageStore} for `seed`. */\nexport function createMockDanmakuStore(\n\tseed: readonly Danmaku[] = [],\n): MockDanmakuStore {\n\tconst rows: Danmaku[] = [...seed]\n\treturn {\n\t\tlist(resId) {\n\t\t\treturn rows.filter((d) => d.anchor.resId === resId)\n\t\t},\n\t\tcreate(resId, input) {\n\t\t\tconst danmaku: Danmaku = {\n\t\t\t\tid: generateId(\"dm\"),\n\t\t\t\tanchor: { resId, data: (input.anchor as { data?: unknown }).data },\n\t\t\t\ttext: input.text,\n\t\t\t\tcolor: \"#fff\",\n\t\t\t\tmode:\n\t\t\t\t\tinput.mode === \"scroll\" ||\n\t\t\t\t\tinput.mode === \"top\" ||\n\t\t\t\t\tinput.mode === \"bottom\"\n\t\t\t\t\t\t? input.mode\n\t\t\t\t\t\t: \"scroll\",\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t}\n\t\t\trows.push(danmaku)\n\t\t\treturn danmaku\n\t\t},\n\t}\n}\n","import type {\n\tDanmakuListFilter,\n\tPluginAssetDeleteResult,\n\tPluginDownloadRequest,\n\tPluginDownloadResult,\n} from \"@hoardodile/sdk-types\"\nimport {\n\ttype HostPush,\n\ttype HostResponse,\n\thostPushKeys,\n\tinvalidatePushKeys,\n\ttype PluginIframeContext,\n\tpluginMethods,\n} from \"@hoardodile/sdk-web\"\nimport { requestSchemas } from \"../host-core/request-schemas.ts\"\nimport type { HostBinding } from \"../host-core/router.ts\"\nimport {\n\tcreateHostRouter,\n\tdefineHandler,\n\ttype HostHandlerEntry,\n} from \"../host-core/router.ts\"\nimport type { MockFileBackend } from \"./file-backends.ts\"\nimport {\n\tcreateMockDanmakuStore,\n\tcreateMockMessageStore,\n\ttype MockDanmakuStore,\n\ttype MockMessageStore,\n} from \"./stores.ts\"\n\n/**\n * The offline host side of the plugin postMessage bridge. Implements the\n * same routing, validation and scoping as the production host (via\n * host-core) with in-memory data — so plugin iframes run with no server\n * at all. Shared by automated component tests (jsdom: register the test\n * window) and the manual workbench page (register the real iframe\n * window).\n */\n\nexport type MockHostLogger = {\n\treadonly info: (message: string, data?: unknown) => void\n\treadonly warn: (message: string, data?: unknown) => void\n\treadonly error: (message: string, data?: unknown) => void\n}\n\nconst defaultLogger: MockHostLogger = {\n\tinfo(message, data) {\n\t\tconsole.log(`[mock-host] ${message}`, data ?? \"\")\n\t},\n\twarn(message, data) {\n\t\tconsole.warn(`[mock-host] ${message}`, data ?? \"\")\n\t},\n\terror(message, data) {\n\t\tconsole.error(`[mock-host] ${message}`, data ?? \"\")\n\t},\n}\n\nexport type MockHostOptions = {\n\t/**\n\t * Window that receives plugin postMessage traffic: the page window in\n\t * a workbench, the test window in jsdom. The host listens on it and\n\t * posts responses to it.\n\t */\n\treadonly targetWindow: Window\n\treadonly files: MockFileBackend\n\treadonly messages?: MockMessageStore\n\treadonly danmaku?: MockDanmakuStore\n\t/** Initial plugin-scoped prefs. */\n\treadonly prefs?: Readonly<Record<string, string>>\n\t/** Initial plugin+resId cache entries. */\n\treadonly cache?: Readonly<Record<string, string>>\n\treadonly logger?: MockHostLogger\n\t/** Called after a plugin writes a pref. */\n\treadonly onPrefChanged?: (key: string, value: string) => void\n\t/** Called after a plugin writes a cache entry. */\n\treadonly onCacheChanged?: (resId: string, key: string, value: string) => void\n\t/**\n\t * Plugin asset vault implementation (workbench only). Absent → the\n\t * asset methods answer `UNAVAILABLE` (tests, jsdom hosts). The real\n\t * app routes these through its server pipeline via tRPC — the mock\n\t * never talks to it.\n\t */\n\treadonly assetVault?: PluginAssetVaultMock\n}\n\n/**\n * The workbench-side plugin asset vault: the mock hands `download` and\n * `deleteAsset` to the host app's implementation (consent dialog + dev\n * server fetch + local vault), mirroring the server pipeline's\n * request/result vocabulary.\n */\nexport type PluginAssetVaultMock = {\n\treadonly download: (\n\t\trequest: PluginDownloadRequest,\n\t) => Promise<PluginDownloadResult>\n\treadonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>\n}\n\nexport type MockHost = {\n\t/**\n\t * Bind a message source to a plugin/resource. The real host binds\n\t * iframe contentWindows; jsdom tests register the test window itself\n\t * (plugin code posts to `window.parent`, which is itself).\n\t */\n\treadonly register: (source: unknown, binding: HostBinding) => void\n\treadonly unregister: (source: unknown) => void\n\t/** Push a host event to one source. */\n\treadonly push: (source: unknown, key: string, data?: unknown) => void\n\t/** Push the plugin context (the iframe mounts on this). */\n\treadonly pushContext: (source: unknown, ctx: PluginIframeContext) => void\n\treadonly setVisibility: (source: unknown, visible: boolean) => void\n\treadonly messages: MockMessageStore\n\treadonly danmaku: MockDanmakuStore\n\treadonly prefs: ReadonlyMap<string, string>\n\treadonly cache: ReadonlyMap<string, string>\n\treadonly dispose: () => void\n}\n\nexport function createMockHost(opts: MockHostOptions): MockHost {\n\tconst targetWindow = opts.targetWindow\n\tconst logger = opts.logger ?? defaultLogger\n\tconst messages = opts.messages ?? createMockMessageStore()\n\tconst danmaku = opts.danmaku ?? createMockDanmakuStore()\n\tconst prefs = new Map(Object.entries(opts.prefs ?? {}))\n\tconst cache = new Map(Object.entries(opts.cache ?? {}))\n\tconst bindings = new Map<unknown, HostBinding>()\n\tconst subscriptions = new Map<unknown, Set<string>>()\n\n\tfunction postToSource(source: unknown, msg: HostPush | HostResponse): void {\n\t\t;(source as Window).postMessage(msg, \"*\")\n\t}\n\n\tfunction pushToSource(source: unknown, key: string, data?: unknown): void {\n\t\tpostToSource(source, { type: \"push\", key, data })\n\t}\n\n\tconst handlers: readonly HostHandlerEntry[] = [\n\t\tdefineHandler(\n\t\t\tpluginMethods.readFile,\n\t\t\trequestSchemas[pluginMethods.readFile],\n\t\t\tasync (ctx, params) => {\n\t\t\t\treturn opts.files.readFile(ctx.resId, params.path, params.range)\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(pluginMethods.listFiles, async (ctx) => {\n\t\t\t// Production answers with the rows the plugin's own listFiles\n\t\t\t// hook produced; a backend that can obtain them serves those.\n\t\t\tconst entries = await opts.files.listFileEntries?.(ctx.resId)\n\t\t\tif (entries !== undefined) return entries\n\t\t\t// No hook (or no way to reach it): the server falls back to\n\t\t\t// bare, naturally sorted filenames — mirror that exactly, or\n\t\t\t// plugins typed against the fallback shape break here only.\n\t\t\tconst names = [...(await opts.files.listFiles(ctx.resId))]\n\t\t\tnames.sort((a, b) =>\n\t\t\t\ta.localeCompare(b, undefined, { sensitivity: \"base\", numeric: true }),\n\t\t\t)\n\t\t\treturn names\n\t\t}),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.logInfo,\n\t\t\trequestSchemas[pluginMethods.logInfo],\n\t\t\t(_ctx, params) => {\n\t\t\t\tlogger.info(logMessage(params), logData(params))\n\t\t\t},\n\t\t),\n\t\tdefineHandler(\n\t\t\tpluginMethods.logWarn,\n\t\t\trequestSchemas[pluginMethods.logWarn],\n\t\t\t(_ctx, params) => {\n\t\t\t\tlogger.warn(logMessage(params), logData(params))\n\t\t\t},\n\t\t),\n\t\tdefineHandler(\n\t\t\tpluginMethods.logError,\n\t\t\trequestSchemas[pluginMethods.logError],\n\t\t\t(_ctx, params) => {\n\t\t\t\tlogger.error(logMessage(params), logData(params))\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(pluginMethods.listMessages, async (ctx) => {\n\t\t\treturn messages.list(ctx.resId)\n\t\t}),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.createMessage,\n\t\t\trequestSchemas[pluginMethods.createMessage],\n\t\t\tasync (ctx, params) => {\n\t\t\t\tconst anchor =\n\t\t\t\t\tparams.anchor === undefined\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: { ...params.anchor, resId: ctx.resId }\n\t\t\t\treturn messages.create(ctx.resId, { body: params.body, anchor })\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.listDanmaku,\n\t\t\trequestSchemas[pluginMethods.listDanmaku],\n\t\t\tasync (ctx, params) => {\n\t\t\t\tconst rows = danmaku.list(ctx.resId)\n\t\t\t\tconst filter = params.filter\n\t\t\t\tif (filter === undefined) return rows\n\t\t\t\treturn rows.filter((d) => matchesDanmakuFilter(d.anchor.data, filter))\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.createDanmaku,\n\t\t\trequestSchemas[pluginMethods.createDanmaku],\n\t\t\tasync (ctx, params) => {\n\t\t\t\treturn danmaku.create(ctx.resId, {\n\t\t\t\t\ttext: params.text,\n\t\t\t\t\tanchor: { ...params.anchor, resId: ctx.resId },\n\t\t\t\t\tmode: params.mode,\n\t\t\t\t})\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.setPref,\n\t\t\trequestSchemas[pluginMethods.setPref],\n\t\t\tasync (_ctx, params) => {\n\t\t\t\tprefs.set(params.key, params.value)\n\t\t\t\topts.onPrefChanged?.(params.key, params.value)\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.setCache,\n\t\t\trequestSchemas[pluginMethods.setCache],\n\t\t\tasync (ctx, params) => {\n\t\t\t\t// A write from a never-bound iframe has nowhere to land —\n\t\t\t\t// drop it silently, same as the production host.\n\t\t\t\tif (ctx.resId === \"\") return\n\t\t\t\tcache.set(`${ctx.resId}:${params.key}`, params.value)\n\t\t\t\topts.onCacheChanged?.(ctx.resId, params.key, params.value)\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.invalidate,\n\t\t\trequestSchemas[pluginMethods.invalidate],\n\t\t\tasync (ctx, params) => {\n\t\t\t\t// Notify the caller so its query hooks refetch — the mock's\n\t\t\t\t// stores are the source of truth, so the refetch returns the\n\t\t\t\t// updated data.\n\t\t\t\tpushToSource(ctx.source, invalidatePushKeys[params.target])\n\t\t\t},\n\t\t),\n\n\t\t// Without a wired asset vault the offline host answers\n\t\t// UNAVAILABLE — the exact vocabulary a plugin sees from a generic\n\t\t// mock. The workbench passes `assetVault` to run the real flow\n\t\t// (consent dialog + dev-server download into a local vault).\n\t\tdefineHandler(\n\t\t\tpluginMethods.download,\n\t\t\trequestSchemas[pluginMethods.download],\n\t\t\tasync (_ctx, params) => {\n\t\t\t\tconst vault = opts.assetVault\n\t\t\t\tif (vault === undefined) throw unavailableAssetError(\"download\")\n\t\t\t\treturn vault.download(params)\n\t\t\t},\n\t\t),\n\t\tdefineHandler(\n\t\t\tpluginMethods.deleteAsset,\n\t\t\trequestSchemas[pluginMethods.deleteAsset],\n\t\t\tasync (_ctx, params) => {\n\t\t\t\tconst vault = opts.assetVault\n\t\t\t\tif (vault === undefined) throw unavailableAssetError(\"deleteAsset\")\n\t\t\t\treturn vault.deleteAsset(params.path)\n\t\t\t},\n\t\t),\n\t]\n\n\tconst router = createHostRouter(handlers, {\n\t\tresolveSource(event) {\n\t\t\t// jsdom delivers same-window messages with source nulled out;\n\t\t\t// real iframes always carry their contentWindow.\n\t\t\tconst source = event.source === null ? targetWindow : event.source\n\t\t\tconst record = bindings.get(source)\n\t\t\tif (record === undefined) return undefined\n\t\t\treturn { source, record }\n\t\t},\n\t\trespond(source, response) {\n\t\t\tpostToSource(source, response)\n\t\t},\n\t\tsubscribe(source, key) {\n\t\t\tlet keys = subscriptions.get(source)\n\t\t\tif (keys === undefined) {\n\t\t\t\tkeys = new Set()\n\t\t\t\tsubscriptions.set(source, keys)\n\t\t\t}\n\t\t\tkeys.add(key)\n\t\t},\n\t})\n\n\tfunction onMessage(event: MessageEvent): void {\n\t\trouter(event)\n\t}\n\ttargetWindow.addEventListener(\"message\", onMessage)\n\n\treturn {\n\t\tregister(source, binding) {\n\t\t\tbindings.set(source, binding)\n\t\t},\n\t\tunregister(source) {\n\t\t\tbindings.delete(source)\n\t\t\tsubscriptions.delete(source)\n\t\t},\n\t\tpush(source, key, data) {\n\t\t\tpushToSource(source, key, data)\n\t\t},\n\t\tpushContext(source, ctx) {\n\t\t\tpushToSource(source, hostPushKeys.context, ctx)\n\t\t},\n\t\tsetVisibility(source, visible) {\n\t\t\tpushToSource(source, hostPushKeys.visibility, { visible })\n\t\t},\n\t\tmessages,\n\t\tdanmaku,\n\t\tprefs,\n\t\tcache,\n\t\tdispose() {\n\t\t\ttargetWindow.removeEventListener(\"message\", onMessage)\n\t\t\tbindings.clear()\n\t\t\tsubscriptions.clear()\n\t\t},\n\t}\n}\n\nfunction logMessage(params: unknown): string {\n\tconst p = params as { message?: unknown } | undefined\n\treturn typeof p?.message === \"string\" ? p.message : String(params)\n}\n\nfunction logData(params: unknown): unknown {\n\tconst p = params as { data?: unknown } | undefined\n\treturn p?.data\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value)\n}\n\n/**\n * Matches a danmaku against the plugin-declared filter: every declared\n * field must equal the value stored under the same key in the anchor's\n * `data` — identical semantics to the production host handler.\n */\nfunction matchesDanmakuFilter(\n\tdata: unknown,\n\tfilter: DanmakuListFilter,\n): boolean {\n\tif (!isRecord(data)) return false\n\tfor (const [key, value] of Object.entries(filter)) {\n\t\tif (value !== undefined && data[key] !== value) return false\n\t}\n\treturn true\n}\n\n/** Machine-readable asset-error rejection (the shared vocabulary). */\nfunction unavailableAssetError(method: string): Error {\n\tconst err = new Error(\n\t\t`${method}() is unavailable in the offline host — plugin asset downloads need the app server runtime`,\n\t)\n\terr.name = \"UNAVAILABLE\"\n\treturn err\n}\n"]}
1
+ {"version":3,"sources":["../src/consent/consent-store.ts","../src/host-core/request-schemas.ts","../src/host-core/router.ts","../src/mock/file-backends.ts","../src/mock/stores.ts","../src/mock/host.ts"],"names":["anchorDataSchema","pluginMethods"],"mappings":";;;;;;AAiDA,IAAI,KAAA,GAAe,EAAE,KAAA,EAAO,EAAC,EAAE;AAC/B,IAAM,SAAA,uBAAgB,GAAA,EAAgB;AAGtC,IAAM,OAAA,uBAAc,GAAA,EAGlB;AAEF,SAAS,SAAS,IAAA,EAAmB;AACpC,EAAA,KAAA,GAAQ,IAAA;AACR,EAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AACjC,IAAA,QAAA,EAAS;AAAA,EACV;AACD;AAEO,SAAS,yBAAyB,QAAA,EAAkC;AAC1E,EAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,EAAA,OAAO,MAAM;AACZ,IAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,EAC1B,CAAA;AACD;AAEO,SAAS,0BAAA,GAAoC;AACnD,EAAA,OAAO,KAAA;AACR;AAGO,SAAS,uBAAuB,KAAA,EAAmC;AACzE,EAAA,KAAA,MAAW,QAAA,IAAY,MAAM,KAAA,EAAO;AACnC,IAAA,IAAI,QAAA,CAAS,QAAA,KAAa,KAAA,CAAM,QAAA,EAAU;AAAA,EAC3C;AACA,EAAA,QAAA,CAAS,EAAE,OAAO,CAAC,GAAG,MAAM,KAAA,EAAO,KAAK,GAAG,CAAA;AAC5C;AAGO,SAAS,qBAAqB,QAAA,EAAwB;AAC5D,EAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,MAAA,CAAO,CAAC,KAAA,KAAU,KAAA,CAAM,aAAa,QAAQ,CAAA;AACvE,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,KAAA,CAAM,KAAA,CAAM,MAAA,EAAQ;AACzC,EAAA,QAAA,CAAS,EAAE,OAAO,CAAA;AACnB;AAMO,SAAS,yBACf,OAAA,EACO;AACP,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAkC;AACvD,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,UAAU,KAAK,CAAA;AAC/D,EAAA,QAAA,CAAS,EAAE,OAAO,CAAC,GAAG,SAAS,MAAA,EAAQ,GAAG,CAAA;AAC3C;AAMO,SAAS,uBACf,KAAA,EACmB;AACnB,EAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,QAAQ,CAAA,EAAG;AAChC,IAAA,OAAO,OAAA,CAAQ,QAAQ,KAAK,CAAA;AAAA,EAC7B;AACA,EAAA,sBAAA,CAAuB,KAAK,CAAA;AAC5B,EAAA,OAAO,IAAI,OAAA,CAAiB,CAAC,OAAA,KAAY;AACxC,IAAA,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,QAAA,EAAU,EAAE,SAAS,CAAA;AAAA,EACxC,CAAC,CAAA;AACF;AAMO,SAAS,qBAAA,CACf,UACA,QAAA,EACO;AACP,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACnC,EAAA,IAAI,WAAW,MAAA,EAAW;AACzB,IAAA,OAAA,CAAQ,OAAO,QAAQ,CAAA;AACvB,IAAA,MAAA,CAAO,QAAQ,QAAQ,CAAA;AAAA,EACxB;AACA,EAAA,oBAAA,CAAqB,QAAQ,CAAA;AAC9B;AAGO,SAAS,oBAAA,GAA6B;AAC5C,EAAA,KAAA,MAAW,UAAU,OAAA,CAAQ,MAAA,EAAO,EAAG,MAAA,CAAO,QAAQ,KAAK,CAAA;AAC3D,EAAA,OAAA,CAAQ,KAAA,EAAM;AACd,EAAA,QAAA,CAAS,EAAE,KAAA,EAAO,EAAC,EAAG,CAAA;AACvB;AC1HO,IAAM,UAAA,GAAaA;AAG1B,IAAM,YAAA,GAAe,EAAE,MAAA,CAAO;AAAA,EAC7B,GAAA,EAAK,EAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,IAAI,CAAA;AAAA,EAC/B,IAAA,EAAM,EAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,GAAG,CAAA;AAAA,EAC/B,QAAQ,CAAA,CACN,MAAA,GACA,KAAA,CAAM,gBAAgB,EACtB,QAAA,EAAS;AAAA,EACX,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,GAAG,EAAE,QAAA;AAC7B,CAAC,CAAA;AAEM,IAAM,cAAA,GAAiB;AAAA,EAC7B,CAAC,aAAA,CAAc,QAAQ,GAAG,EAAE,MAAA,CAAO;AAAA,IAClC,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,IACtB,KAAA,EAAO,EACL,MAAA,CAAO;AAAA,MACP,KAAA,EAAO,EAAE,MAAA,EAAO,CAAE,KAAI,CAAE,WAAA,GAAc,QAAA,EAAS;AAAA,MAC/C,GAAA,EAAK,EAAE,MAAA,EAAO,CAAE,KAAI,CAAE,WAAA,GAAc,QAAA;AAAS,KAC7C,EACA,QAAA;AAAS,GACX,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,SAAS,GAAG,MAAA;AAAA,EAC3B,CAAC,aAAA,CAAc,YAAY,GAAG,MAAA;AAAA,EAC9B,CAAC,aAAA,CAAc,aAAa,GAAG,EAAE,MAAA,CAAO;AAAA,IACvC,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,IACtB,MAAA,EAAQ,WAAW,QAAA;AAAS,GAC5B,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,WAAW,GAAG,EAAE,MAAA,CAAO;AAAA;AAAA;AAAA,IAGrC,MAAA,EAAQ,EACN,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,MAAM,CAAC,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,SAAS,CAAC,CAAC,CAAA,CACjE,QAAA;AAAS,GACX,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,aAAa,GAAG,EAAE,MAAA,CAAO;AAAA,IACvC,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,IACtB,MAAA,EAAQ,UAAA;AAAA,IACR,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AAAS,GAC1B,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,OAAO,GAAG,EAAE,MAAA,CAAO;AAAA,IACjC,GAAA,EAAK,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,IACrB,KAAA,EAAO,EAAE,MAAA;AAAO,GAChB,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,QAAQ,GAAG,EAAE,MAAA,CAAO;AAAA,IAClC,GAAA,EAAK,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,IACrB,KAAA,EAAO,EAAE,MAAA;AAAO,GAChB,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,UAAU,GAAG,EAAE,MAAA,CAAO;AAAA,IACpC,MAAA,EAAQ,EAAE,IAAA,CAAK,CAAC,YAAY,WAAA,EAAa,UAAA,EAAY,SAAS,CAAC;AAAA,GAC/D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKD,CAAC,aAAA,CAAc,QAAQ,GAAG,EAAE,KAAA,CAAM;AAAA,IACjC,YAAA;AAAA,IACA,CAAA,CAAE,MAAM,YAAY,CAAA,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,4BAA4B;AAAA,GAC7D,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,WAAW,GAAG,EAAE,MAAA,CAAO;AAAA,IACrC,IAAA,EAAM,EAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,GAAG;AAAA,GAC/B,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,OAAO,GAAG,EAAE,MAAA,CAAO;AAAA,IACjC,OAAA,EAAS,EAAE,MAAA,EAAO;AAAA,IAClB,IAAA,EAAM,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,MAAA,IAAU,CAAA,CAAE,OAAA,EAAS,CAAA,CAAE,QAAA;AAAS,GACjD,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,OAAO,GAAG,EAAE,MAAA,CAAO;AAAA,IACjC,OAAA,EAAS,EAAE,MAAA,EAAO;AAAA,IAClB,IAAA,EAAM,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,MAAA,IAAU,CAAA,CAAE,OAAA,EAAS,CAAA,CAAE,QAAA;AAAS,GACjD,CAAA;AAAA,EACD,CAAC,aAAA,CAAc,QAAQ,GAAG,EAAE,MAAA,CAAO;AAAA,IAClC,OAAA,EAAS,EAAE,MAAA,EAAO;AAAA,IAClB,IAAA,EAAM,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,MAAA,IAAU,CAAA,CAAE,OAAA,EAAS,CAAA,CAAE,QAAA;AAAS,GACjD;AACF;ACrBO,SAAS,aAAA,CACf,MAAA,EACA,eAAA,EAGA,YAAA,EAImB;AACnB,EAAA,IAAI,OAAO,oBAAoB,UAAA,EAAY;AAC1C,IAAA,OAAO;AAAA,MACN,MAAA;AAAA,MACA,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,OAAO,GAAA,KAAQ,eAAA,CAAgB,GAAG;AAAA,KAC5C;AAAA,EACD;AACA,EAAA,IAAI,iBAAiB,MAAA,EAAW;AAC/B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,eAAA,EAAkB,MAAM,CAAA,2BAAA,CAA6B,CAAA;AAAA,EACtE;AACA,EAAA,OAAO;AAAA,IACN,MAAA;AAAA,IACA,MAAA,EAAQ,eAAA;AAAA,IACR,SAAS,OAAO,GAAA,EAAK,MAAA,KAAW,YAAA,CAAa,KAAK,MAAM;AAAA,GACzD;AACD;AAOO,SAAS,gBAAA,CACf,UACA,IAAA,EACgC;AAChC,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAyC;AAC9D,EAAA,MAAM,OAAA,uBAAc,GAAA,EAA0B;AAC9C,EAAA,KAAA,MAAW,SAAS,QAAA,EAAU;AAC7B,IAAA,IAAI,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,MAAM,CAAA,EAAG;AAC/B,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,KAAA,CAAM,MAAM,CAAA,CAAE,CAAA;AAAA,IAC5D;AACA,IAAA,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,MAAA,EAAQ,KAAA,CAAM,OAAO,CAAA;AACxC,IAAA,IAAI,KAAA,CAAM,WAAW,MAAA,EAAW,OAAA,CAAQ,IAAI,KAAA,CAAM,MAAA,EAAQ,MAAM,MAAM,CAAA;AAAA,EACvE;AAIA,EAAA,MAAM,aAAA,uBAAoB,OAAA,EAAgB;AAE1C,EAAA,OAAO,SAAS,cAAc,KAAA,EAAqB;AAClD,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,aAAA,CAAc,KAAK,CAAA;AACzC,IAAA,IAAI,aAAa,MAAA,EAAW;AAC5B,IAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,QAAA;AAE3B,IAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAClB,IAAA,IAAI,OAAO,IAAA,IAAQ,OAAO,QAAQ,QAAA,IAAY,GAAA,CAAI,SAAS,MAAA,EAAW;AACrE,MAAA;AAAA,IACD;AAOA,IAAA,MAAM,QAAS,GAAA,CAA4B,KAAA;AAC3C,IAAA,IAAI,UAAU,gBAAA,IAAoB,CAAC,aAAA,CAAc,GAAA,CAAI,MAAgB,CAAA,EAAG;AACvE,MAAA,aAAA,CAAc,IAAI,MAAgB,CAAA;AAClC,MAAA,IAAI,UAAU,MAAA,EAAW;AACxB,QAAA,OAAA,CAAQ,IAAA;AAAA,UACP,CAAA,0BAAA,EAA6B,MAAA,CAAO,QAAQ,CAAA,yGAAA,EAAuG,gBAAgB,CAAA,6CAAA;AAAA,SACpK;AAAA,MACD,CAAA,MAAO;AACN,QAAA,OAAA,CAAQ,IAAA;AAAA,UACP,CAAA,0BAAA,EAA6B,OAAO,QAAQ,CAAA,0BAAA,EAA6B,OAAO,KAAK,CAAC,yBAAyB,gBAAgB,CAAA,+EAAA;AAAA,SAChI;AAAA,MACD;AAAA,IACD;AAEA,IAAA,IAAI,GAAA,CAAI,SAAS,WAAA,EAAa;AAC7B,MAAA,IAAA,CAAK,SAAA,CAAU,MAAA,EAAQ,GAAA,CAAI,GAAG,CAAA;AAC9B,MAAA;AAAA,IACD;AAEA,IAAA,IAAI,GAAA,CAAI,SAAS,SAAA,EAAW;AAE5B,IAAA,SAAS,QAAQ,QAAA,EAA8B;AAC9C,MAAA,IAAA,CAAK,OAAA,CAAQ,QAAQ,QAAQ,CAAA;AAAA,IAC9B;AASA,IAAA,IACC,OAAO,GAAA,CAAI,KAAA,KAAU,QAAA,IACrB,GAAA,CAAI,UAAU,EAAA,IACd,GAAA,CAAI,KAAA,KAAU,MAAA,CAAO,KAAA,EACpB;AACD,MAAA,OAAA,CAAQ,EAAE,MAAM,UAAA,EAAY,EAAA,EAAI,IAAI,EAAA,EAAI,EAAA,EAAI,MAAM,CAAA;AAClD,MAAA;AAAA,IACD;AAEA,IAAA,MAAM,GAAA,GAA0B;AAAA,MAC/B,MAAA;AAAA,MACA,OAAO,MAAA,CAAO,KAAA;AAAA,MACd,UAAU,MAAA,CAAO;AAAA,KAClB;AAEA,IAAA,MAAM,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA;AACvC,IAAA,IAAI,YAAY,MAAA,EAAW;AAC1B,MAAA,OAAA,CAAQ;AAAA,QACP,IAAA,EAAM,UAAA;AAAA,QACN,IAAI,GAAA,CAAI,EAAA;AAAA,QACR,EAAA,EAAI,KAAA;AAAA,QACJ,KAAA,EAAO,CAAA,gBAAA,EAAmB,GAAA,CAAI,MAAM,CAAA;AAAA,OACpC,CAAA;AACD,MAAA;AAAA,IACD;AAEA,IAAA,IAAI,MAAA;AACJ,IAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA;AACrC,IAAA,IAAI,WAAW,MAAA,EAAW;AACzB,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,SAAA,CAAU,GAAA,CAAI,MAAM,CAAA;AAC1C,MAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACpB,QAAA,OAAA,CAAQ;AAAA,UACP,IAAA,EAAM,UAAA;AAAA,UACN,IAAI,GAAA,CAAI,EAAA;AAAA,UACR,EAAA,EAAI,KAAA;AAAA,UACJ,OAAO,CAAA,mBAAA,EAAsB,GAAA,CAAI,MAAM,CAAA,EAAA,EAAK,MAAA,CAAO,MAAM,OAAO,CAAA;AAAA,SAChE,CAAA;AACD,QAAA;AAAA,MACD;AACA,MAAA,MAAA,GAAS,MAAA,CAAO,IAAA;AAAA,IACjB,CAAA,MAAO;AACN,MAAA,MAAA,GAAS,GAAA,CAAI,MAAA;AAAA,IACd;AAEA,IAAA,OAAA,CAAQ,GAAA,EAAK,MAAM,CAAA,CACjB,IAAA,CAAK,CAAC,IAAA,KAAS;AACf,MAAA,OAAA,CAAQ,EAAE,MAAM,UAAA,EAAY,EAAA,EAAI,IAAI,EAAA,EAAI,EAAA,EAAI,IAAA,EAAM,IAAA,EAAM,CAAA;AAAA,IACzD,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAQ;AACf,MAAA,MAAM,YACL,GAAA,YAAe,KAAA,IAAS,IAAI,IAAA,KAAS,OAAA,GAAU,IAAI,IAAA,GAAO,MAAA;AAC3D,MAAA,OAAA,CAAQ;AAAA,QACP,IAAA,EAAM,UAAA;AAAA,QACN,IAAI,GAAA,CAAI,EAAA;AAAA,QACR,EAAA,EAAI,KAAA;AAAA,QACJ,OAAO,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAAA;AAAA;AAAA;AAAA,QAItD,SAAA,EAAW,SAAA;AAAA,QACX;AAAA,OACA,CAAA;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAA;AACD;;;ACtMO,SAAS,yBAAA,CACf,KAAA,GAAuD,EAAC,EACtC;AAClB,EAAA,OAAO;AAAA,IACN,MAAM,SAAA,GAAY;AACjB,MAAA,OAAO,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACzB,CAAA;AAAA,IACA,MAAM,QAAA,CAAS,MAAA,EAAQ,IAAA,EAAM,KAAA,EAAO;AACnC,MAAA,MAAM,OAAA,GAAU,MAAM,IAAI,CAAA;AAC1B,MAAA,IAAI,YAAY,MAAA,EAAW;AAC1B,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,+BAAA,EAAkC,IAAI,CAAA,CAAE,CAAA;AAAA,MACzD;AACA,MAAA,MAAM,KAAA,GACL,OAAO,OAAA,KAAY,QAAA,GAChB,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,OAAO,CAAA,GAChC,OAAA;AAGJ,MAAA,IAAI,KAAA,KAAU,MAAA,EAAW,OAAO,KAAA,CAAM,OAAM,CAAE,MAAA;AAC9C,MAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,SAAS,CAAC,CAAA;AAC1C,MAAA,MAAM,GAAA,GAAM,KAAK,GAAA,CAAI,KAAA,CAAM,OAAO,KAAA,CAAM,MAAA,EAAQ,MAAM,MAAM,CAAA;AAC5D,MAAA,OAAO,KAAA,CAAM,KAAA,CAAM,KAAA,EAAO,GAAG,CAAA,CAAE,MAAA;AAAA,IAChC,CAAA;AAAA,IACA,MAAM,QAAA,CAAS,MAAA,EAAQ,IAAA,EAAM;AAC5B,MAAA,MAAM,OAAA,GAAU,MAAM,IAAI,CAAA;AAC1B,MAAA,IAAI,OAAA,KAAY,QAAW,OAAO,MAAA;AAClC,MAAA,MAAM,KAAA,GACL,OAAO,OAAA,KAAY,QAAA,GAChB,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,OAAO,CAAA,GAChC,OAAA;AACJ,MAAA,OAAO,EAAE,SAAA,EAAW,KAAA,CAAM,UAAA,EAAW;AAAA,IACtC;AAAA,GACD;AACD;;;AC5DA,IAAI,MAAA,GAAS,CAAA;AAEb,SAAS,WAAW,MAAA,EAAwB;AAC3C,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,MAAA,EAAQ,CAAA,CAAA;AAC7B;AAeO,SAAS,sBAAA,CACf,IAAA,GAA2B,EAAC,EACT;AACnB,EAAA,MAAM,IAAA,GAAkB,CAAC,GAAG,IAAI,CAAA;AAChC,EAAA,OAAO;AAAA,IACN,KAAK,KAAA,EAAO;AACX,MAAA,OAAO,IAAA,CAAK,OAAO,CAAC,CAAA,KAAM,EAAE,MAAA,CAAO,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,IACnD,CAAA;AAAA,IACA,MAAA,CAAO,OAAO,KAAA,EAAO;AACpB,MAAA,MAAM,OAAA,GAAmB;AAAA,QACxB,EAAA,EAAI,WAAW,KAAK,CAAA;AAAA,QACpB,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,QACpB,SAAS,EAAC;AAAA,QACV,MAAA,EAAQ,CAAC,KAAK,CAAA;AAAA,QACd,SAAA,EAAW,CAAA;AAAA,QACX,YAAA,EAAc,CAAA;AAAA,QACd,UAAA,EAAY,CAAA;AAAA,QACZ,QAAQ,KAAA,CAAM;AAAA,OACf;AACA,MAAA,IAAA,CAAK,KAAK,OAAO,CAAA;AACjB,MAAA,OAAO,OAAA;AAAA,IACR;AAAA,GACD;AACD;AAWO,SAAS,sBAAA,CACf,IAAA,GAA2B,EAAC,EACT;AACnB,EAAA,MAAM,IAAA,GAAkB,CAAC,GAAG,IAAI,CAAA;AAChC,EAAA,OAAO;AAAA,IACN,KAAK,KAAA,EAAO;AACX,MAAA,OAAO,KAAK,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,MAAA,CAAO,UAAU,KAAK,CAAA;AAAA,IACnD,CAAA;AAAA,IACA,MAAA,CAAO,OAAO,KAAA,EAAO;AACpB,MAAA,MAAM,OAAA,GAAmB;AAAA,QACxB,EAAA,EAAI,WAAW,IAAI,CAAA;AAAA,QACnB,QAAQ,EAAE,KAAA,EAAO,IAAA,EAAO,KAAA,CAAM,OAA8B,IAAA,EAAK;AAAA,QACjE,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,KAAA,EAAO,MAAA;AAAA,QACP,IAAA,EACC,KAAA,CAAM,IAAA,KAAS,QAAA,IACf,KAAA,CAAM,IAAA,KAAS,KAAA,IACf,KAAA,CAAM,IAAA,KAAS,QAAA,GACZ,KAAA,CAAM,IAAA,GACN,QAAA;AAAA,QACJ,SAAA,EAAW,KAAK,GAAA;AAAI,OACrB;AACA,MAAA,IAAA,CAAK,KAAK,OAAO,CAAA;AACjB,MAAA,OAAO,OAAA;AAAA,IACR;AAAA,GACD;AACD;;;AC5CA,IAAM,aAAA,GAAgC;AAAA,EACrC,IAAA,CAAK,SAAS,IAAA,EAAM;AACnB,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,YAAA,EAAe,OAAO,CAAA,CAAA,EAAI,QAAQ,EAAE,CAAA;AAAA,EACjD,CAAA;AAAA,EACA,IAAA,CAAK,SAAS,IAAA,EAAM;AACnB,IAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,YAAA,EAAe,OAAO,CAAA,CAAA,EAAI,QAAQ,EAAE,CAAA;AAAA,EAClD,CAAA;AAAA,EACA,KAAA,CAAM,SAAS,IAAA,EAAM;AACpB,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,YAAA,EAAe,OAAO,CAAA,CAAA,EAAI,QAAQ,EAAE,CAAA;AAAA,EACnD;AACD,CAAA;AA+DO,SAAS,eAAe,IAAA,EAAiC;AAC/D,EAAA,MAAM,eAAe,IAAA,CAAK,YAAA;AAC1B,EAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,aAAA;AAC9B,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,QAAA,IAAY,sBAAA,EAAuB;AACzD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,IAAW,sBAAA,EAAuB;AACvD,EAAA,MAAM,KAAA,GAAQ,IAAI,GAAA,CAAI,MAAA,CAAO,QAAQ,IAAA,CAAK,KAAA,IAAS,EAAE,CAAC,CAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,IAAI,GAAA,CAAI,MAAA,CAAO,QAAQ,IAAA,CAAK,KAAA,IAAS,EAAE,CAAC,CAAA;AACtD,EAAA,MAAM,QAAA,uBAAe,GAAA,EAA0B;AAC/C,EAAA,MAAM,aAAA,uBAAoB,GAAA,EAA0B;AAEpD,EAAA,SAAS,YAAA,CAAa,QAAiB,GAAA,EAAoC;AACzE,IAAC,MAAA,CAAkB,WAAA,CAAY,GAAA,EAAK,GAAG,CAAA;AAAA,EACzC;AAEA,EAAA,SAAS,YAAA,CAAa,MAAA,EAAiB,GAAA,EAAa,IAAA,EAAsB;AACzE,IAAA,YAAA,CAAa,QAAQ,EAAE,IAAA,EAAM,MAAA,EAAQ,GAAA,EAAK,MAAM,CAAA;AAAA,EACjD;AAEA,EAAA,MAAM,QAAA,GAAwC;AAAA,IAC7C,aAAA;AAAA,MACCC,aAAAA,CAAc,QAAA;AAAA,MACd,cAAA,CAAeA,cAAc,QAAQ,CAAA;AAAA,MACrC,OAAO,KAAK,MAAA,KAAW;AACtB,QAAA,OAAO,IAAA,CAAK,MAAM,QAAA,CAAS,GAAA,CAAI,OAAO,MAAA,CAAO,IAAA,EAAM,OAAO,KAAK,CAAA;AAAA,MAChE;AAAA,KACD;AAAA,IAEA,aAAA,CAAcA,aAAAA,CAAc,SAAA,EAAW,OAAO,GAAA,KAAQ;AAGrD,MAAA,MAAM,UAAU,MAAM,IAAA,CAAK,KAAA,CAAM,eAAA,GAAkB,IAAI,KAAK,CAAA;AAC5D,MAAA,IAAI,OAAA,KAAY,QAAW,OAAO,OAAA;AAIlC,MAAA,MAAM,KAAA,GAAQ,CAAC,GAAI,MAAM,KAAK,KAAA,CAAM,SAAA,CAAU,GAAA,CAAI,KAAK,CAAE,CAAA;AACzD,MAAA,KAAA,CAAM,IAAA;AAAA,QAAK,CAAC,CAAA,EAAG,CAAA,KACd,CAAA,CAAE,aAAA,CAAc,CAAA,EAAG,MAAA,EAAW,EAAE,WAAA,EAAa,MAAA,EAAQ,OAAA,EAAS,IAAA,EAAM;AAAA,OACrE;AACA,MAAA,OAAO,KAAA;AAAA,IACR,CAAC,CAAA;AAAA,IAED,aAAA;AAAA,MACCA,aAAAA,CAAc,OAAA;AAAA,MACd,cAAA,CAAeA,cAAc,OAAO,CAAA;AAAA,MACpC,CAAC,MAAM,MAAA,KAAW;AACjB,QAAA,MAAA,CAAO,KAAK,UAAA,CAAW,MAAM,CAAA,EAAG,OAAA,CAAQ,MAAM,CAAC,CAAA;AAAA,MAChD;AAAA,KACD;AAAA,IACA,aAAA;AAAA,MACCA,aAAAA,CAAc,OAAA;AAAA,MACd,cAAA,CAAeA,cAAc,OAAO,CAAA;AAAA,MACpC,CAAC,MAAM,MAAA,KAAW;AACjB,QAAA,MAAA,CAAO,KAAK,UAAA,CAAW,MAAM,CAAA,EAAG,OAAA,CAAQ,MAAM,CAAC,CAAA;AAAA,MAChD;AAAA,KACD;AAAA,IACA,aAAA;AAAA,MACCA,aAAAA,CAAc,QAAA;AAAA,MACd,cAAA,CAAeA,cAAc,QAAQ,CAAA;AAAA,MACrC,CAAC,MAAM,MAAA,KAAW;AACjB,QAAA,MAAA,CAAO,MAAM,UAAA,CAAW,MAAM,CAAA,EAAG,OAAA,CAAQ,MAAM,CAAC,CAAA;AAAA,MACjD;AAAA,KACD;AAAA,IAEA,aAAA,CAAcA,aAAAA,CAAc,YAAA,EAAc,OAAO,GAAA,KAAQ;AACxD,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AAAA,IAC/B,CAAC,CAAA;AAAA,IAED,aAAA;AAAA,MACCA,aAAAA,CAAc,aAAA;AAAA,MACd,cAAA,CAAeA,cAAc,aAAa,CAAA;AAAA,MAC1C,OAAO,KAAK,MAAA,KAAW;AACtB,QAAA,MAAM,MAAA,GACL,MAAA,CAAO,MAAA,KAAW,MAAA,GACf,MAAA,GACA,EAAE,GAAG,MAAA,CAAO,MAAA,EAAQ,KAAA,EAAO,GAAA,CAAI,KAAA,EAAM;AACzC,QAAA,OAAO,QAAA,CAAS,OAAO,GAAA,CAAI,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,CAAO,IAAA,EAAM,MAAA,EAAQ,CAAA;AAAA,MAChE;AAAA,KACD;AAAA,IAEA,aAAA;AAAA,MACCA,aAAAA,CAAc,WAAA;AAAA,MACd,cAAA,CAAeA,cAAc,WAAW,CAAA;AAAA,MACxC,OAAO,KAAK,MAAA,KAAW;AACtB,QAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AACnC,QAAA,MAAM,SAAS,MAAA,CAAO,MAAA;AACtB,QAAA,IAAI,MAAA,KAAW,QAAW,OAAO,IAAA;AACjC,QAAA,OAAO,IAAA,CAAK,OAAO,CAAC,CAAA,KAAM,qBAAqB,CAAA,CAAE,MAAA,CAAO,IAAA,EAAM,MAAM,CAAC,CAAA;AAAA,MACtE;AAAA,KACD;AAAA,IAEA,aAAA;AAAA,MACCA,aAAAA,CAAc,aAAA;AAAA,MACd,cAAA,CAAeA,cAAc,aAAa,CAAA;AAAA,MAC1C,OAAO,KAAK,MAAA,KAAW;AACtB,QAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,KAAA,EAAO;AAAA,UAChC,MAAM,MAAA,CAAO,IAAA;AAAA,UACb,QAAQ,EAAE,GAAG,OAAO,MAAA,EAAQ,KAAA,EAAO,IAAI,KAAA,EAAM;AAAA,UAC7C,MAAM,MAAA,CAAO;AAAA,SACb,CAAA;AAAA,MACF;AAAA,KACD;AAAA,IAEA,aAAA;AAAA,MACCA,aAAAA,CAAc,OAAA;AAAA,MACd,cAAA,CAAeA,cAAc,OAAO,CAAA;AAAA,MACpC,OAAO,MAAM,MAAA,KAAW;AACvB,QAAA,KAAA,CAAM,GAAA,CAAI,MAAA,CAAO,GAAA,EAAK,MAAA,CAAO,KAAK,CAAA;AAClC,QAAA,IAAA,CAAK,aAAA,GAAgB,MAAA,CAAO,GAAA,EAAK,MAAA,CAAO,KAAK,CAAA;AAAA,MAC9C;AAAA,KACD;AAAA,IAEA,aAAA;AAAA,MACCA,aAAAA,CAAc,QAAA;AAAA,MACd,cAAA,CAAeA,cAAc,QAAQ,CAAA;AAAA,MACrC,OAAO,KAAK,MAAA,KAAW;AAGtB,QAAA,IAAI,GAAA,CAAI,UAAU,EAAA,EAAI;AACtB,QAAA,KAAA,CAAM,GAAA,CAAI,GAAG,GAAA,CAAI,KAAK,IAAI,MAAA,CAAO,GAAG,CAAA,CAAA,EAAI,MAAA,CAAO,KAAK,CAAA;AACpD,QAAA,IAAA,CAAK,iBAAiB,GAAA,CAAI,KAAA,EAAO,MAAA,CAAO,GAAA,EAAK,OAAO,KAAK,CAAA;AAAA,MAC1D;AAAA,KACD;AAAA,IAEA,aAAA;AAAA,MACCA,aAAAA,CAAc,UAAA;AAAA,MACd,cAAA,CAAeA,cAAc,UAAU,CAAA;AAAA,MACvC,OAAO,KAAK,MAAA,KAAW;AAItB,QAAA,YAAA,CAAa,GAAA,CAAI,MAAA,EAAQ,kBAAA,CAAmB,MAAA,CAAO,MAAM,CAAC,CAAA;AAAA,MAC3D;AAAA,KACD;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,aAAA;AAAA,MACCA,aAAAA,CAAc,QAAA;AAAA,MACd,cAAA,CAAeA,cAAc,QAAQ,CAAA;AAAA,MACrC,OAAO,MAAM,MAAA,KAAW;AACvB,QAAA,MAAM,QAAQ,IAAA,CAAK,UAAA;AACnB,QAAA,IAAI,KAAA,KAAU,MAAA,EAAW,MAAM,qBAAA,CAAsB,UAAU,CAAA;AAC/D,QAAA,OAAO,KAAA,CAAM,SAAS,MAAM,CAAA;AAAA,MAC7B;AAAA,KACD;AAAA,IACA,aAAA;AAAA,MACCA,aAAAA,CAAc,WAAA;AAAA,MACd,cAAA,CAAeA,cAAc,WAAW,CAAA;AAAA,MACxC,OAAO,MAAM,MAAA,KAAW;AACvB,QAAA,MAAM,QAAQ,IAAA,CAAK,UAAA;AACnB,QAAA,IAAI,KAAA,KAAU,MAAA,EAAW,MAAM,qBAAA,CAAsB,aAAa,CAAA;AAClE,QAAA,OAAO,KAAA,CAAM,WAAA,CAAY,MAAA,CAAO,IAAI,CAAA;AAAA,MACrC;AAAA;AACD,GACD;AAEA,EAAA,MAAM,MAAA,GAAS,iBAAiB,QAAA,EAAU;AAAA,IACzC,cAAc,KAAA,EAAO;AAGpB,MAAA,MAAM,MAAA,GAAS,KAAA,CAAM,MAAA,KAAW,IAAA,GAAO,eAAe,KAAA,CAAM,MAAA;AAC5D,MAAA,MAAM,MAAA,GAAS,QAAA,CAAS,GAAA,CAAI,MAAM,CAAA;AAClC,MAAA,IAAI,MAAA,KAAW,QAAW,OAAO,MAAA;AACjC,MAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAAA,IACzB,CAAA;AAAA,IACA,OAAA,CAAQ,QAAQ,QAAA,EAAU;AACzB,MAAA,YAAA,CAAa,QAAQ,QAAQ,CAAA;AAAA,IAC9B,CAAA;AAAA,IACA,SAAA,CAAU,QAAQ,GAAA,EAAK;AACtB,MAAA,IAAI,IAAA,GAAO,aAAA,CAAc,GAAA,CAAI,MAAM,CAAA;AACnC,MAAA,IAAI,SAAS,MAAA,EAAW;AACvB,QAAA,IAAA,uBAAW,GAAA,EAAI;AACf,QAAA,aAAA,CAAc,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAA,MAC/B;AACA,MAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AAAA,IACb;AAAA,GACA,CAAA;AAED,EAAA,SAAS,UAAU,KAAA,EAA2B;AAC7C,IAAA,MAAA,CAAO,KAAK,CAAA;AAAA,EACb;AACA,EAAA,YAAA,CAAa,gBAAA,CAAiB,WAAW,SAAS,CAAA;AAElD,EAAA,OAAO;AAAA,IACN,QAAA,CAAS,QAAQ,OAAA,EAAS;AACzB,MAAA,QAAA,CAAS,GAAA,CAAI,QAAQ,OAAO,CAAA;AAAA,IAC7B,CAAA;AAAA,IACA,WAAW,MAAA,EAAQ;AAClB,MAAA,QAAA,CAAS,OAAO,MAAM,CAAA;AACtB,MAAA,aAAA,CAAc,OAAO,MAAM,CAAA;AAAA,IAC5B,CAAA;AAAA,IACA,IAAA,CAAK,MAAA,EAAQ,GAAA,EAAK,IAAA,EAAM;AACvB,MAAA,YAAA,CAAa,MAAA,EAAQ,KAAK,IAAI,CAAA;AAAA,IAC/B,CAAA;AAAA,IACA,WAAA,CAAY,QAAQ,GAAA,EAAK;AACxB,MAAA,YAAA,CAAa,MAAA,EAAQ,YAAA,CAAa,OAAA,EAAS,GAAG,CAAA;AAAA,IAC/C,CAAA;AAAA,IACA,aAAA,CAAc,QAAQ,OAAA,EAAS;AAC9B,MAAA,YAAA,CAAa,MAAA,EAAQ,YAAA,CAAa,UAAA,EAAY,EAAE,SAAS,CAAA;AAAA,IAC1D,CAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,KAAA;AAAA,IACA,KAAA;AAAA,IACA,OAAA,GAAU;AACT,MAAA,YAAA,CAAa,mBAAA,CAAoB,WAAW,SAAS,CAAA;AACrD,MAAA,QAAA,CAAS,KAAA,EAAM;AACf,MAAA,aAAA,CAAc,KAAA,EAAM;AAAA,IACrB;AAAA,GACD;AACD;AAEA,SAAS,WAAW,MAAA,EAAyB;AAC5C,EAAA,MAAM,CAAA,GAAI,MAAA;AACV,EAAA,OAAO,OAAO,CAAA,EAAG,OAAA,KAAY,WAAW,CAAA,CAAE,OAAA,GAAU,OAAO,MAAM,CAAA;AAClE;AAEA,SAAS,QAAQ,MAAA,EAA0B;AAC1C,EAAA,MAAM,CAAA,GAAI,MAAA;AACV,EAAA,OAAO,CAAA,EAAG,IAAA;AACX;AAEA,SAAS,SAAS,KAAA,EAAkD;AACnE,EAAA,OAAO,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,YAAY,CAAC,KAAA,CAAM,QAAQ,KAAK,CAAA;AAC3E;AAOA,SAAS,oBAAA,CACR,MACA,MAAA,EACU;AACV,EAAA,IAAI,CAAC,QAAA,CAAS,IAAI,CAAA,EAAG,OAAO,KAAA;AAC5B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AAClD,IAAA,IAAI,UAAU,MAAA,IAAa,IAAA,CAAK,GAAG,CAAA,KAAM,OAAO,OAAO,KAAA;AAAA,EACxD;AACA,EAAA,OAAO,IAAA;AACR;AAGA,SAAS,sBAAsB,MAAA,EAAuB;AACrD,EAAA,MAAM,MAAM,IAAI,KAAA;AAAA,IACf,GAAG,MAAM,CAAA,+FAAA;AAAA,GACV;AACA,EAAA,GAAA,CAAI,IAAA,GAAO,aAAA;AACX,EAAA,OAAO,GAAA;AACR","file":"index.js","sourcesContent":["/**\n * The consent-dialog queue for plugin asset downloads — the shared\n * implementation both host apps mount:\n *\n * - the **app** feeds it from SSE (`pluginDownloadRequested` /\n * `pluginDownloadResolved`) and drives decisions through the server\n * (`pluginAsset.decide`); the resolved broadcast closes entries here;\n * - the **workbench** has no server: its asset engine enqueues via\n * {@link request} and the dialog answers via {@link decide} — the same\n * dialog component, one queue per page, FIFO, one dialog at a time.\n *\n * State-only by design: decisions never mutate the queue directly\n * except through `close`/`decide`, which the two hosts wire to their\n * own decision path.\n */\n\n/**\n * One item of a batch consent question: a single download's URL and\n * vault destination, exactly as the dialog lists it.\n */\nexport type DownloadConsentItem = {\n\treadonly url: string\n\treadonly dest: string\n\treadonly sizeBytes?: number\n\treadonly reason?: string\n}\n\n/**\n * A queued consent question: the ticket shape. Structurally identical to\n * the server's `pluginDownloadRequested` SSE event minus its `type`\n * discriminator (and to `pluginAsset.listPending` rows) — declared here\n * instead of importing `@hoardodile/schemas` so the host-core package\n * stays dependency-lean.\n *\n * One entry = one dialog listing every item (a single download is an\n * items array of one; a batched plugin call is one entry for the whole\n * batch).\n */\nexport type DownloadConsentEntry = {\n\treadonly ticketId: string\n\treadonly pluginId: string\n\treadonly pluginName: string\n\treadonly items: readonly DownloadConsentItem[]\n}\n\ntype State = {\n\treadonly queue: readonly DownloadConsentEntry[]\n}\n\nlet state: State = { queue: [] }\nconst listeners = new Set<() => void>()\n\n/** Engine side: tickets awaiting a local decision (workbench path). */\nconst pending = new Map<\n\tstring,\n\t{ readonly resolve: (approved: boolean) => void }\n>()\n\nfunction setState(next: State): void {\n\tstate = next\n\tfor (const listener of listeners) {\n\t\tlistener()\n\t}\n}\n\nexport function subscribeDownloadConsent(listener: () => void): () => void {\n\tlisteners.add(listener)\n\treturn () => {\n\t\tlisteners.delete(listener)\n\t}\n}\n\nexport function getDownloadConsentSnapshot(): State {\n\treturn state\n}\n\n/** A new ticket arrived (SSE) or was requested locally: append if absent. */\nexport function enqueueDownloadConsent(entry: DownloadConsentEntry): void {\n\tfor (const existing of state.queue) {\n\t\tif (existing.ticketId === entry.ticketId) return\n\t}\n\tsetState({ queue: [...state.queue, entry] })\n}\n\n/** A ticket resolved (decide/timeout/dispose): drop it in every consumer. */\nexport function closeDownloadConsent(ticketId: string): void {\n\tconst queue = state.queue.filter((entry) => entry.ticketId !== ticketId)\n\tif (queue.length === state.queue.length) return\n\tsetState({ queue })\n}\n\n/**\n * Replace the queue from a pending list (app: `listPending` after an SSE\n * reconnect; workbench: same API on restart).\n */\nexport function rehydrateDownloadConsent(\n\tentries: readonly DownloadConsentEntry[],\n): void {\n\tconst byTicket = new Map<string, DownloadConsentEntry>()\n\tfor (const entry of entries) byTicket.set(entry.ticketId, entry)\n\tsetState({ queue: [...byTicket.values()] })\n}\n\n/**\n * Enqueue a ticket and wait for its decision (workbench engine path).\n * The dialog's answer arrives through {@link decideDownloadConsent}.\n */\nexport function requestDownloadConsent(\n\tentry: DownloadConsentEntry,\n): Promise<boolean> {\n\tif (pending.has(entry.ticketId)) {\n\t\treturn Promise.resolve(false)\n\t}\n\tenqueueDownloadConsent(entry)\n\treturn new Promise<boolean>((resolve) => {\n\t\tpending.set(entry.ticketId, { resolve })\n\t})\n}\n\n/**\n * Answer a ticket from the dialog. Closes the entry and resolves any\n * awaiting engine call. Idempotent; unknown tickets are a no-op.\n */\nexport function decideDownloadConsent(\n\tticketId: string,\n\tapproved: boolean,\n): void {\n\tconst waiter = pending.get(ticketId)\n\tif (waiter !== undefined) {\n\t\tpending.delete(ticketId)\n\t\twaiter.resolve(approved)\n\t}\n\tcloseDownloadConsent(ticketId)\n}\n\n/** Test-only: reset the singleton between tests. */\nexport function resetDownloadConsent(): void {\n\tfor (const waiter of pending.values()) waiter.resolve(false)\n\tpending.clear()\n\tsetState({ queue: [] })\n}\n","import { PLUGIN_ASSET_BATCH_MAX_ITEMS } from \"@hoardodile/sdk-types/plugin-asset-limits\"\nimport { anchorData as anchorDataSchema } from \"@hoardodile/sdk-types/schema\"\nimport type { PluginRequests } from \"@hoardodile/sdk-web\"\nimport { pluginMethods } from \"@hoardodile/sdk-web\"\nimport { z } from \"zod\"\n\n/**\n * Wire-level param schemas for every request method, shared by the real\n * host (apps/web) and the offline mock host. Both hosts validate\n * identical shapes, so a plugin that passes validation in the mock is\n * guaranteed to pass in production.\n */\n\n/**\n * Wire shape of a message/danmaku anchor: the plugin location payload\n * only. The resource id is host state — the host stamps its own binding\n * into the anchor before it reaches the server.\n */\nexport const anchorData = anchorDataSchema\n\n/** One download request (a batch is an array of these). */\nconst downloadItem = z.object({\n\turl: z.string().min(1).max(2048),\n\tdest: z.string().min(1).max(256),\n\tsha256: z\n\t\t.string()\n\t\t.regex(/^[0-9a-f]{64}$/)\n\t\t.optional(),\n\treason: z.string().max(200).optional(),\n})\n\nexport const requestSchemas = {\n\t[pluginMethods.readFile]: z.object({\n\t\tpath: z.string().min(1),\n\t\trange: z\n\t\t\t.object({\n\t\t\t\tstart: z.number().int().nonnegative().optional(),\n\t\t\t\tend: z.number().int().nonnegative().optional(),\n\t\t\t})\n\t\t\t.optional(),\n\t}),\n\t[pluginMethods.listFiles]: undefined,\n\t[pluginMethods.listMessages]: undefined,\n\t[pluginMethods.createMessage]: z.object({\n\t\tbody: z.string().min(1),\n\t\tanchor: anchorData.optional(),\n\t}),\n\t[pluginMethods.listDanmaku]: z.object({\n\t\t// Filter keys are plugin-defined vocabulary; matching is generic\n\t\t// strict equality against the anchor's data.\n\t\tfilter: z\n\t\t\t.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))\n\t\t\t.optional(),\n\t}),\n\t[pluginMethods.createDanmaku]: z.object({\n\t\ttext: z.string().min(1),\n\t\tanchor: anchorData,\n\t\tmode: z.string().optional(),\n\t}),\n\t[pluginMethods.setPref]: z.object({\n\t\tkey: z.string().min(1),\n\t\tvalue: z.string(),\n\t}),\n\t[pluginMethods.setCache]: z.object({\n\t\tkey: z.string().min(1),\n\t\tvalue: z.string(),\n\t}),\n\t[pluginMethods.invalidate]: z.object({\n\t\ttarget: z.enum([\"resource\", \"resources\", \"messages\", \"danmaku\"]),\n\t}),\n\t// The URL is length-capped but not URI-validated here: the host parses\n\t// it server-side and answers with a machine-readable POLICY error. A\n\t// single request or a batch (one consent dialog listing every item,\n\t// all-or-nothing) — capped so a burst cannot stack unbounded tickets.\n\t[pluginMethods.download]: z.union([\n\t\tdownloadItem,\n\t\tz.array(downloadItem).min(1).max(PLUGIN_ASSET_BATCH_MAX_ITEMS),\n\t]),\n\t[pluginMethods.deleteAsset]: z.object({\n\t\tpath: z.string().min(1).max(256),\n\t}),\n\t[pluginMethods.logInfo]: z.object({\n\t\tmessage: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t}),\n\t[pluginMethods.logWarn]: z.object({\n\t\tmessage: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t}),\n\t[pluginMethods.logError]: z.object({\n\t\tmessage: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t}),\n} satisfies Record<keyof PluginRequests, z.ZodTypeAny | undefined>\n","import {\n\ttype HostResponse,\n\ttype PluginMessage,\n\tPROTOCOL_VERSION,\n} from \"@hoardodile/sdk-web\"\nimport type { z } from \"zod\"\n\n/**\n * The shared host-side protocol core: message demux, method routing,\n * per-method param validation, stale-request scoping and the response\n * envelope. Both the real host (apps/web) and the offline mock host\n * assemble on this module, so routing and validation never drift.\n */\n\n/** What a registered iframe (or mock window) is bound to. */\nexport type HostBinding = {\n\treadonly pluginId: string\n\treadonly resId: string\n}\n\nexport type HostRouterDeps = {\n\t/**\n\t * Authenticate an inbound message event: narrow it to a trusted source\n\t * + binding, or return `undefined` to drop the message. The real host\n\t * validates origin/source against its iframe registry; the mock\n\t * validates against its own registered windows.\n\t */\n\treadonly resolveSource: (\n\t\tevent: MessageEvent,\n\t) => { readonly source: unknown; readonly record: HostBinding } | undefined\n\t/** Send a response back to the source (the layer's postMessage exit). */\n\treadonly respond: (source: unknown, response: HostResponse) => void\n\t/** Record a subscription key for the source. */\n\treadonly subscribe: (source: unknown, key: string) => void\n}\n\nexport type HostHandlerContext = {\n\treadonly source: unknown\n\t/**\n\t * The resource this request is scoped to: the source's current\n\t * binding (\"\" only for never-bound sources). Requests stamped by the\n\t * SDK with a different resource are dropped as stale before they\n\t * reach a handler.\n\t */\n\treadonly resId: string\n\treadonly pluginId: string\n}\n\nexport type HostHandlerEntry = {\n\treadonly method: string\n\t/** Param schema; validated in the router before the handler runs. */\n\treadonly schema: z.ZodTypeAny | undefined\n\treadonly handler: (\n\t\tctx: HostHandlerContext,\n\t\tparams: unknown,\n\t) => Promise<unknown>\n}\n\nexport function defineHandler<TReturn>(\n\tmethod: string,\n\thandler: (ctx: HostHandlerContext) => Promise<TReturn> | TReturn,\n): HostHandlerEntry\n\nexport function defineHandler<TSchema extends z.ZodTypeAny, TReturn>(\n\tmethod: string,\n\tschema: TSchema,\n\thandler: (\n\t\tctx: HostHandlerContext,\n\t\tparams: z.infer<TSchema>,\n\t) => Promise<TReturn> | TReturn,\n): HostHandlerEntry\n\nexport function defineHandler(\n\tmethod: string,\n\tschemaOrHandler:\n\t\t| z.ZodTypeAny\n\t\t| ((ctx: HostHandlerContext) => Promise<unknown> | unknown),\n\tmaybeHandler?: (\n\t\tctx: HostHandlerContext,\n\t\tparams: unknown,\n\t) => Promise<unknown> | unknown,\n): HostHandlerEntry {\n\tif (typeof schemaOrHandler === \"function\") {\n\t\treturn {\n\t\t\tmethod,\n\t\t\tschema: undefined,\n\t\t\thandler: async (ctx) => schemaOrHandler(ctx),\n\t\t}\n\t}\n\tif (maybeHandler === undefined) {\n\t\tthrow new Error(`defineHandler(\"${method}\") called without a handler`)\n\t}\n\treturn {\n\t\tmethod,\n\t\tschema: schemaOrHandler,\n\t\thandler: async (ctx, params) => maybeHandler(ctx, params),\n\t}\n}\n\n/**\n * Create the per-window message handler: authenticates the event,\n * routes requests by method, validates params, drops stale scopes and\n * wraps every outcome in the response envelope.\n */\nexport function createHostRouter(\n\thandlers: readonly HostHandlerEntry[],\n\tdeps: HostRouterDeps,\n): (event: MessageEvent) => void {\n\tconst registry = new Map<string, HostHandlerEntry[\"handler\"]>()\n\tconst schemas = new Map<string, z.ZodTypeAny>()\n\tfor (const entry of handlers) {\n\t\tif (registry.has(entry.method)) {\n\t\t\tthrow new Error(`Duplicate handler method: ${entry.method}`)\n\t\t}\n\t\tregistry.set(entry.method, entry.handler)\n\t\tif (entry.schema !== undefined) schemas.set(entry.method, entry.schema)\n\t}\n\n\t// Warn once per source so a mismatched plugin build does not spam the\n\t// console on every message, while still surfacing the problem loudly.\n\tconst warnedSources = new WeakSet<object>()\n\n\treturn function handleMessage(event: MessageEvent) {\n\t\tconst resolved = deps.resolveSource(event)\n\t\tif (resolved === undefined) return\n\t\tconst { source, record } = resolved\n\n\t\tconst msg = event.data as PluginMessage\n\t\tif (msg == null || typeof msg !== \"object\" || msg.type === undefined) {\n\t\t\treturn\n\t\t}\n\n\t\t// Protocol handshake: every SDK-build plugin stamps its messages\n\t\t// with the protocol version it was built against. A mismatch (or a\n\t\t// missing stamp — an old plugin build) means the wire contract may\n\t\t// have drifted; the message still routes so old plugins keep\n\t\t// working, but the developer gets a loud warning.\n\t\tconst proto = (msg as { proto?: unknown }).proto\n\t\tif (proto !== PROTOCOL_VERSION && !warnedSources.has(source as object)) {\n\t\t\twarnedSources.add(source as object)\n\t\t\tif (proto === undefined) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[host-web] plugin iframe (${record.pluginId}) did not stamp its protocol version — it was built against an older SDK. Assuming PROTOCOL_VERSION ${PROTOCOL_VERSION}; rebuild the plugin to silence this warning.`,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[host-web] plugin iframe (${record.pluginId}) speaks protocol version ${String(proto)} but this host speaks ${PROTOCOL_VERSION} — the plugin was built against a different SDK version and may misbehave.`,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tif (msg.type === \"subscribe\") {\n\t\t\tdeps.subscribe(source, msg.key)\n\t\t\treturn\n\t\t}\n\n\t\tif (msg.type !== \"request\") return\n\n\t\tfunction respond(response: HostResponse): void {\n\t\t\tdeps.respond(source, response)\n\t\t}\n\n\t\t// The SDK stamps each request with the resource it was issued for\n\t\t// (PluginRequest.resId). A stamp that no longer matches the\n\t\t// binding marks the request as stale — the tree that issued it is\n\t\t// gone (e.g. an unmount flush racing a rebind) — so it is dropped\n\t\t// silently instead of leaking into the wrong resource. Unstamped\n\t\t// requests (older plugin builds) use the current binding, which\n\t\t// outlives release, so late flushes after a close still land.\n\t\tif (\n\t\t\ttypeof msg.resId === \"string\" &&\n\t\t\tmsg.resId !== \"\" &&\n\t\t\tmsg.resId !== record.resId\n\t\t) {\n\t\t\trespond({ type: \"response\", id: msg.id, ok: true })\n\t\t\treturn\n\t\t}\n\n\t\tconst ctx: HostHandlerContext = {\n\t\t\tsource,\n\t\t\tresId: record.resId,\n\t\t\tpluginId: record.pluginId,\n\t\t}\n\n\t\tconst handler = registry.get(msg.method)\n\t\tif (handler === undefined) {\n\t\t\trespond({\n\t\t\t\ttype: \"response\",\n\t\t\t\tid: msg.id,\n\t\t\t\tok: false,\n\t\t\t\terror: `Unknown method: ${msg.method}`,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tlet params: unknown\n\t\tconst schema = schemas.get(msg.method)\n\t\tif (schema !== undefined) {\n\t\t\tconst parsed = schema.safeParse(msg.params)\n\t\t\tif (!parsed.success) {\n\t\t\t\trespond({\n\t\t\t\t\ttype: \"response\",\n\t\t\t\t\tid: msg.id,\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: `Invalid params for ${msg.method}: ${parsed.error.message}`,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tparams = parsed.data\n\t\t} else {\n\t\t\tparams = msg.params\n\t\t}\n\n\t\thandler(ctx, params)\n\t\t\t.then((data) => {\n\t\t\t\trespond({ type: \"response\", id: msg.id, ok: true, data })\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconst errorName =\n\t\t\t\t\terr instanceof Error && err.name !== \"Error\" ? err.name : undefined\n\t\t\t\trespond({\n\t\t\t\t\ttype: \"response\",\n\t\t\t\t\tid: msg.id,\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t// Machine-readable plugin error code (`DENIED`/…)\n\t\t\t\t\t// travels as `errorCode`; `errorName` is the legacy\n\t\t\t\t\t// alias (same value) so older SDK builds keep working.\n\t\t\t\t\terrorCode: errorName,\n\t\t\t\t\terrorName,\n\t\t\t\t})\n\t\t\t})\n\t}\n}\n","import type { ReadFileRange } from \"@hoardodile/sdk-types\"\n\n/** Shared read-range contract; defined once in `@hoardodile/sdk-types`. */\nexport type { ReadFileRange } from \"@hoardodile/sdk-types\"\n\n/**\n * File backends for the offline mock host: the plugin's `listFiles` and\n * `readFile` requests resolve against this interface, so a mock can be\n * pointed at an in-memory map, a real directory (Node), or a read-only\n * HTTP mount (workbench) without changing the host.\n */\nexport type MockFileBackend = {\n\treadonly listFiles: (resId: string) => Promise<readonly string[]>\n\treadonly readFile: (\n\t\tresId: string,\n\t\tpath: string,\n\t\trange?: ReadFileRange,\n\t) => Promise<ArrayBuffer>\n\treadonly statFile: (\n\t\tresId: string,\n\t\tpath: string,\n\t) => Promise<{ readonly sizeBytes: number } | undefined>\n\t/**\n\t * The rows the plugin's own `listFiles` hook produced, when the\n\t * backend can obtain them (the workbench reads them from a sandboxed\n\t * hook snapshot). In production the host answers `listFiles` with\n\t * exactly these plugin-shaped entries; returning `undefined` falls\n\t * back to generic `{filename, ext, sizeBytes}` rows.\n\t */\n\treadonly listFileEntries?: (\n\t\tresId: string,\n\t) => Promise<readonly unknown[] | undefined>\n}\n\n/** In-memory file map backend for unit tests. `resId` is ignored. */\nexport function createInMemoryFileBackend(\n\tfiles: Readonly<Record<string, string | Uint8Array>> = {},\n): MockFileBackend {\n\treturn {\n\t\tasync listFiles() {\n\t\t\treturn Object.keys(files)\n\t\t},\n\t\tasync readFile(_resId, path, range) {\n\t\t\tconst content = files[path]\n\t\t\tif (content === undefined) {\n\t\t\t\tthrow new Error(`mock file backend has no entry ${path}`)\n\t\t\t}\n\t\t\tconst bytes =\n\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t? new TextEncoder().encode(content)\n\t\t\t\t\t: content\n\t\t\t// Mirrors host semantics: the range is clamped to the content\n\t\t\t// size; a start at or past the end resolves to an empty result.\n\t\t\tif (range === undefined) return bytes.slice().buffer\n\t\t\tconst start = Math.max(0, range.start ?? 0)\n\t\t\tconst end = Math.min(range.end ?? bytes.length, bytes.length)\n\t\t\treturn bytes.slice(start, end).buffer\n\t\t},\n\t\tasync statFile(_resId, path) {\n\t\t\tconst content = files[path]\n\t\t\tif (content === undefined) return undefined\n\t\t\tconst bytes =\n\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t? new TextEncoder().encode(content)\n\t\t\t\t\t: content\n\t\t\treturn { sizeBytes: bytes.byteLength }\n\t\t},\n\t}\n}\n","import type { Danmaku, Message } from \"@hoardodile/sdk-types\"\n\n/**\n * In-memory stores backing the offline mock host's message and danmaku\n * handlers. Rows are shaped like the server's responses so plugin UI\n * tests exercise the real consumption paths.\n */\n\nlet nextId = 1\n\nfunction generateId(prefix: string): string {\n\treturn `${prefix}-${nextId++}`\n}\n\nexport type MockMessageStore = {\n\treadonly list: (resId: string) => readonly Message[]\n\treadonly create: (\n\t\tresId: string,\n\t\tinput: { body: string; anchor?: unknown },\n\t) => Message\n}\n\n/**\n * `seed` pre-fills the store with rows the plugin should already see —\n * the workbench passes the resource's real comments so the iframe opens\n * with the same content the app would show.\n */\nexport function createMockMessageStore(\n\tseed: readonly Message[] = [],\n): MockMessageStore {\n\tconst rows: Message[] = [...seed]\n\treturn {\n\t\tlist(resId) {\n\t\t\treturn rows.filter((m) => m.resIds.includes(resId))\n\t\t},\n\t\tcreate(resId, input) {\n\t\t\tconst message: Message = {\n\t\t\t\tid: generateId(\"msg\"),\n\t\t\t\tbody: input.body,\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t\tcharIds: [],\n\t\t\t\tresIds: [resId],\n\t\t\t\tlikeCount: 0,\n\t\t\t\tdislikeCount: 0,\n\t\t\t\treplyCount: 0,\n\t\t\t\tanchor: input.anchor as Message[\"anchor\"],\n\t\t\t}\n\t\t\trows.push(message)\n\t\t\treturn message\n\t\t},\n\t}\n}\n\nexport type MockDanmakuStore = {\n\treadonly list: (resId: string) => readonly Danmaku[]\n\treadonly create: (\n\t\tresId: string,\n\t\tinput: { text: string; anchor: unknown; mode?: string },\n\t) => Danmaku\n}\n\n/** See {@link createMockMessageStore} for `seed`. */\nexport function createMockDanmakuStore(\n\tseed: readonly Danmaku[] = [],\n): MockDanmakuStore {\n\tconst rows: Danmaku[] = [...seed]\n\treturn {\n\t\tlist(resId) {\n\t\t\treturn rows.filter((d) => d.anchor.resId === resId)\n\t\t},\n\t\tcreate(resId, input) {\n\t\t\tconst danmaku: Danmaku = {\n\t\t\t\tid: generateId(\"dm\"),\n\t\t\t\tanchor: { resId, data: (input.anchor as { data?: unknown }).data },\n\t\t\t\ttext: input.text,\n\t\t\t\tcolor: \"#fff\",\n\t\t\t\tmode:\n\t\t\t\t\tinput.mode === \"scroll\" ||\n\t\t\t\t\tinput.mode === \"top\" ||\n\t\t\t\t\tinput.mode === \"bottom\"\n\t\t\t\t\t\t? input.mode\n\t\t\t\t\t\t: \"scroll\",\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t}\n\t\t\trows.push(danmaku)\n\t\t\treturn danmaku\n\t\t},\n\t}\n}\n","import type {\n\tDanmakuListFilter,\n\tPluginAssetDeleteResult,\n\tPluginDownloadRequest,\n\tPluginDownloadResult,\n} from \"@hoardodile/sdk-types\"\nimport {\n\ttype HostPush,\n\ttype HostResponse,\n\thostPushKeys,\n\tinvalidatePushKeys,\n\ttype PluginIframeContext,\n\tpluginMethods,\n} from \"@hoardodile/sdk-web\"\nimport { requestSchemas } from \"../host-core/request-schemas.ts\"\nimport type { HostBinding } from \"../host-core/router.ts\"\nimport {\n\tcreateHostRouter,\n\tdefineHandler,\n\ttype HostHandlerEntry,\n} from \"../host-core/router.ts\"\nimport type { MockFileBackend } from \"./file-backends.ts\"\nimport {\n\tcreateMockDanmakuStore,\n\tcreateMockMessageStore,\n\ttype MockDanmakuStore,\n\ttype MockMessageStore,\n} from \"./stores.ts\"\n\n/**\n * The offline host side of the plugin postMessage bridge. Implements the\n * same routing, validation and scoping as the production host (via\n * host-core) with in-memory data — so plugin iframes run with no server\n * at all. Shared by automated component tests (jsdom: register the test\n * window) and the manual workbench page (register the real iframe\n * window).\n */\n\nexport type MockHostLogger = {\n\treadonly info: (message: string, data?: unknown) => void\n\treadonly warn: (message: string, data?: unknown) => void\n\treadonly error: (message: string, data?: unknown) => void\n}\n\nconst defaultLogger: MockHostLogger = {\n\tinfo(message, data) {\n\t\tconsole.log(`[mock-host] ${message}`, data ?? \"\")\n\t},\n\twarn(message, data) {\n\t\tconsole.warn(`[mock-host] ${message}`, data ?? \"\")\n\t},\n\terror(message, data) {\n\t\tconsole.error(`[mock-host] ${message}`, data ?? \"\")\n\t},\n}\n\nexport type MockHostOptions = {\n\t/**\n\t * Window that receives plugin postMessage traffic: the page window in\n\t * a workbench, the test window in jsdom. The host listens on it and\n\t * posts responses to it.\n\t */\n\treadonly targetWindow: Window\n\treadonly files: MockFileBackend\n\treadonly messages?: MockMessageStore\n\treadonly danmaku?: MockDanmakuStore\n\t/** Initial plugin-scoped prefs. */\n\treadonly prefs?: Readonly<Record<string, string>>\n\t/** Initial plugin+resId cache entries. */\n\treadonly cache?: Readonly<Record<string, string>>\n\treadonly logger?: MockHostLogger\n\t/** Called after a plugin writes a pref. */\n\treadonly onPrefChanged?: (key: string, value: string) => void\n\t/** Called after a plugin writes a cache entry. */\n\treadonly onCacheChanged?: (resId: string, key: string, value: string) => void\n\t/**\n\t * Plugin asset vault implementation (workbench only). Absent → the\n\t * asset methods answer `UNAVAILABLE` (tests, jsdom hosts). The real\n\t * app routes these through its server pipeline via tRPC — the mock\n\t * never talks to it.\n\t */\n\treadonly assetVault?: PluginAssetVaultMock\n}\n\n/**\n * The workbench-side plugin asset vault: the mock hands `download` and\n * `deleteAsset` to the host app's implementation (consent dialog + dev\n * server fetch + local vault), mirroring the server pipeline's\n * request/result vocabulary.\n */\nexport type PluginAssetVaultMock = {\n\treadonly download: (\n\t\trequest: PluginDownloadRequest | readonly PluginDownloadRequest[],\n\t) => Promise<PluginDownloadResult | readonly PluginDownloadResult[]>\n\treadonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>\n}\n\nexport type MockHost = {\n\t/**\n\t * Bind a message source to a plugin/resource. The real host binds\n\t * iframe contentWindows; jsdom tests register the test window itself\n\t * (plugin code posts to `window.parent`, which is itself).\n\t */\n\treadonly register: (source: unknown, binding: HostBinding) => void\n\treadonly unregister: (source: unknown) => void\n\t/** Push a host event to one source. */\n\treadonly push: (source: unknown, key: string, data?: unknown) => void\n\t/** Push the plugin context (the iframe mounts on this). */\n\treadonly pushContext: (source: unknown, ctx: PluginIframeContext) => void\n\treadonly setVisibility: (source: unknown, visible: boolean) => void\n\treadonly messages: MockMessageStore\n\treadonly danmaku: MockDanmakuStore\n\treadonly prefs: ReadonlyMap<string, string>\n\treadonly cache: ReadonlyMap<string, string>\n\treadonly dispose: () => void\n}\n\nexport function createMockHost(opts: MockHostOptions): MockHost {\n\tconst targetWindow = opts.targetWindow\n\tconst logger = opts.logger ?? defaultLogger\n\tconst messages = opts.messages ?? createMockMessageStore()\n\tconst danmaku = opts.danmaku ?? createMockDanmakuStore()\n\tconst prefs = new Map(Object.entries(opts.prefs ?? {}))\n\tconst cache = new Map(Object.entries(opts.cache ?? {}))\n\tconst bindings = new Map<unknown, HostBinding>()\n\tconst subscriptions = new Map<unknown, Set<string>>()\n\n\tfunction postToSource(source: unknown, msg: HostPush | HostResponse): void {\n\t\t;(source as Window).postMessage(msg, \"*\")\n\t}\n\n\tfunction pushToSource(source: unknown, key: string, data?: unknown): void {\n\t\tpostToSource(source, { type: \"push\", key, data })\n\t}\n\n\tconst handlers: readonly HostHandlerEntry[] = [\n\t\tdefineHandler(\n\t\t\tpluginMethods.readFile,\n\t\t\trequestSchemas[pluginMethods.readFile],\n\t\t\tasync (ctx, params) => {\n\t\t\t\treturn opts.files.readFile(ctx.resId, params.path, params.range)\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(pluginMethods.listFiles, async (ctx) => {\n\t\t\t// Production answers with the rows the plugin's own listFiles\n\t\t\t// hook produced; a backend that can obtain them serves those.\n\t\t\tconst entries = await opts.files.listFileEntries?.(ctx.resId)\n\t\t\tif (entries !== undefined) return entries\n\t\t\t// No hook (or no way to reach it): the server falls back to\n\t\t\t// bare, naturally sorted filenames — mirror that exactly, or\n\t\t\t// plugins typed against the fallback shape break here only.\n\t\t\tconst names = [...(await opts.files.listFiles(ctx.resId))]\n\t\t\tnames.sort((a, b) =>\n\t\t\t\ta.localeCompare(b, undefined, { sensitivity: \"base\", numeric: true }),\n\t\t\t)\n\t\t\treturn names\n\t\t}),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.logInfo,\n\t\t\trequestSchemas[pluginMethods.logInfo],\n\t\t\t(_ctx, params) => {\n\t\t\t\tlogger.info(logMessage(params), logData(params))\n\t\t\t},\n\t\t),\n\t\tdefineHandler(\n\t\t\tpluginMethods.logWarn,\n\t\t\trequestSchemas[pluginMethods.logWarn],\n\t\t\t(_ctx, params) => {\n\t\t\t\tlogger.warn(logMessage(params), logData(params))\n\t\t\t},\n\t\t),\n\t\tdefineHandler(\n\t\t\tpluginMethods.logError,\n\t\t\trequestSchemas[pluginMethods.logError],\n\t\t\t(_ctx, params) => {\n\t\t\t\tlogger.error(logMessage(params), logData(params))\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(pluginMethods.listMessages, async (ctx) => {\n\t\t\treturn messages.list(ctx.resId)\n\t\t}),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.createMessage,\n\t\t\trequestSchemas[pluginMethods.createMessage],\n\t\t\tasync (ctx, params) => {\n\t\t\t\tconst anchor =\n\t\t\t\t\tparams.anchor === undefined\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: { ...params.anchor, resId: ctx.resId }\n\t\t\t\treturn messages.create(ctx.resId, { body: params.body, anchor })\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.listDanmaku,\n\t\t\trequestSchemas[pluginMethods.listDanmaku],\n\t\t\tasync (ctx, params) => {\n\t\t\t\tconst rows = danmaku.list(ctx.resId)\n\t\t\t\tconst filter = params.filter\n\t\t\t\tif (filter === undefined) return rows\n\t\t\t\treturn rows.filter((d) => matchesDanmakuFilter(d.anchor.data, filter))\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.createDanmaku,\n\t\t\trequestSchemas[pluginMethods.createDanmaku],\n\t\t\tasync (ctx, params) => {\n\t\t\t\treturn danmaku.create(ctx.resId, {\n\t\t\t\t\ttext: params.text,\n\t\t\t\t\tanchor: { ...params.anchor, resId: ctx.resId },\n\t\t\t\t\tmode: params.mode,\n\t\t\t\t})\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.setPref,\n\t\t\trequestSchemas[pluginMethods.setPref],\n\t\t\tasync (_ctx, params) => {\n\t\t\t\tprefs.set(params.key, params.value)\n\t\t\t\topts.onPrefChanged?.(params.key, params.value)\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.setCache,\n\t\t\trequestSchemas[pluginMethods.setCache],\n\t\t\tasync (ctx, params) => {\n\t\t\t\t// A write from a never-bound iframe has nowhere to land —\n\t\t\t\t// drop it silently, same as the production host.\n\t\t\t\tif (ctx.resId === \"\") return\n\t\t\t\tcache.set(`${ctx.resId}:${params.key}`, params.value)\n\t\t\t\topts.onCacheChanged?.(ctx.resId, params.key, params.value)\n\t\t\t},\n\t\t),\n\n\t\tdefineHandler(\n\t\t\tpluginMethods.invalidate,\n\t\t\trequestSchemas[pluginMethods.invalidate],\n\t\t\tasync (ctx, params) => {\n\t\t\t\t// Notify the caller so its query hooks refetch — the mock's\n\t\t\t\t// stores are the source of truth, so the refetch returns the\n\t\t\t\t// updated data.\n\t\t\t\tpushToSource(ctx.source, invalidatePushKeys[params.target])\n\t\t\t},\n\t\t),\n\n\t\t// Without a wired asset vault the offline host answers\n\t\t// UNAVAILABLE — the exact vocabulary a plugin sees from a generic\n\t\t// mock. The workbench passes `assetVault` to run the real flow\n\t\t// (consent dialog + dev-server download into a local vault).\n\t\tdefineHandler(\n\t\t\tpluginMethods.download,\n\t\t\trequestSchemas[pluginMethods.download],\n\t\t\tasync (_ctx, params) => {\n\t\t\t\tconst vault = opts.assetVault\n\t\t\t\tif (vault === undefined) throw unavailableAssetError(\"download\")\n\t\t\t\treturn vault.download(params)\n\t\t\t},\n\t\t),\n\t\tdefineHandler(\n\t\t\tpluginMethods.deleteAsset,\n\t\t\trequestSchemas[pluginMethods.deleteAsset],\n\t\t\tasync (_ctx, params) => {\n\t\t\t\tconst vault = opts.assetVault\n\t\t\t\tif (vault === undefined) throw unavailableAssetError(\"deleteAsset\")\n\t\t\t\treturn vault.deleteAsset(params.path)\n\t\t\t},\n\t\t),\n\t]\n\n\tconst router = createHostRouter(handlers, {\n\t\tresolveSource(event) {\n\t\t\t// jsdom delivers same-window messages with source nulled out;\n\t\t\t// real iframes always carry their contentWindow.\n\t\t\tconst source = event.source === null ? targetWindow : event.source\n\t\t\tconst record = bindings.get(source)\n\t\t\tif (record === undefined) return undefined\n\t\t\treturn { source, record }\n\t\t},\n\t\trespond(source, response) {\n\t\t\tpostToSource(source, response)\n\t\t},\n\t\tsubscribe(source, key) {\n\t\t\tlet keys = subscriptions.get(source)\n\t\t\tif (keys === undefined) {\n\t\t\t\tkeys = new Set()\n\t\t\t\tsubscriptions.set(source, keys)\n\t\t\t}\n\t\t\tkeys.add(key)\n\t\t},\n\t})\n\n\tfunction onMessage(event: MessageEvent): void {\n\t\trouter(event)\n\t}\n\ttargetWindow.addEventListener(\"message\", onMessage)\n\n\treturn {\n\t\tregister(source, binding) {\n\t\t\tbindings.set(source, binding)\n\t\t},\n\t\tunregister(source) {\n\t\t\tbindings.delete(source)\n\t\t\tsubscriptions.delete(source)\n\t\t},\n\t\tpush(source, key, data) {\n\t\t\tpushToSource(source, key, data)\n\t\t},\n\t\tpushContext(source, ctx) {\n\t\t\tpushToSource(source, hostPushKeys.context, ctx)\n\t\t},\n\t\tsetVisibility(source, visible) {\n\t\t\tpushToSource(source, hostPushKeys.visibility, { visible })\n\t\t},\n\t\tmessages,\n\t\tdanmaku,\n\t\tprefs,\n\t\tcache,\n\t\tdispose() {\n\t\t\ttargetWindow.removeEventListener(\"message\", onMessage)\n\t\t\tbindings.clear()\n\t\t\tsubscriptions.clear()\n\t\t},\n\t}\n}\n\nfunction logMessage(params: unknown): string {\n\tconst p = params as { message?: unknown } | undefined\n\treturn typeof p?.message === \"string\" ? p.message : String(params)\n}\n\nfunction logData(params: unknown): unknown {\n\tconst p = params as { data?: unknown } | undefined\n\treturn p?.data\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value)\n}\n\n/**\n * Matches a danmaku against the plugin-declared filter: every declared\n * field must equal the value stored under the same key in the anchor's\n * `data` — identical semantics to the production host handler.\n */\nfunction matchesDanmakuFilter(\n\tdata: unknown,\n\tfilter: DanmakuListFilter,\n): boolean {\n\tif (!isRecord(data)) return false\n\tfor (const [key, value] of Object.entries(filter)) {\n\t\tif (value !== undefined && data[key] !== value) return false\n\t}\n\treturn true\n}\n\n/** Machine-readable asset-error rejection (the shared vocabulary). */\nfunction unavailableAssetError(method: string): Error {\n\tconst err = new Error(\n\t\t`${method}() is unavailable in the offline host — plugin asset downloads need the app server runtime`,\n\t)\n\terr.name = \"UNAVAILABLE\"\n\treturn err\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoardodile/host-web",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "description": "Browser-side hoardodile host runtime: host-core protocol router and the offline mock host.",
6
6
  "keywords": [
@@ -41,9 +41,9 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "zod": "^4.4.3",
44
- "@hoardodile/host": "0.0.0",
45
- "@hoardodile/sdk-types": "0.0.0",
46
- "@hoardodile/sdk-web": "0.0.0"
44
+ "@hoardodile/host": "0.1.1",
45
+ "@hoardodile/sdk-web": "0.1.1",
46
+ "@hoardodile/sdk-types": "0.1.1"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^26.2.0",
@@ -16,8 +16,12 @@ function ticket(ticketId: string, dest = "runtime.mjs"): DownloadConsentEntry {
16
16
  ticketId,
17
17
  pluginId: "p-1",
18
18
  pluginName: "Test",
19
- url: "https://example.com/runtime.mjs",
20
- dest,
19
+ items: [
20
+ {
21
+ url: "https://example.com/runtime.mjs",
22
+ dest,
23
+ },
24
+ ],
21
25
  }
22
26
  }
23
27
 
@@ -34,6 +38,20 @@ describe("download consent store", () => {
34
38
  expect(queue.map((t) => t.ticketId)).toEqual(["a1", "a2"])
35
39
  })
36
40
 
41
+ it("a batch entry is one queue item carrying every item", () => {
42
+ enqueueDownloadConsent({
43
+ ...ticket("batch"),
44
+ items: [
45
+ { url: "https://example.com/a.mjs", dest: "a.mjs" },
46
+ { url: "https://example.com/b.mjs", dest: "b.mjs" },
47
+ { url: "https://example.com/c.mjs", dest: "c.mjs" },
48
+ ],
49
+ })
50
+ const { queue } = getDownloadConsentSnapshot()
51
+ expect(queue).toHaveLength(1)
52
+ expect(queue[0]!.items).toHaveLength(3)
53
+ })
54
+
37
55
  it("closes a ticket by id and notifies subscribers", () => {
38
56
  let notified = 0
39
57
  const unsubscribe = subscribeDownloadConsent(() => {
@@ -14,21 +14,33 @@
14
14
  * own decision path.
15
15
  */
16
16
 
17
+ /**
18
+ * One item of a batch consent question: a single download's URL and
19
+ * vault destination, exactly as the dialog lists it.
20
+ */
21
+ export type DownloadConsentItem = {
22
+ readonly url: string
23
+ readonly dest: string
24
+ readonly sizeBytes?: number
25
+ readonly reason?: string
26
+ }
27
+
17
28
  /**
18
29
  * A queued consent question: the ticket shape. Structurally identical to
19
30
  * the server's `pluginDownloadRequested` SSE event minus its `type`
20
31
  * discriminator (and to `pluginAsset.listPending` rows) — declared here
21
32
  * instead of importing `@hoardodile/schemas` so the host-core package
22
33
  * stays dependency-lean.
34
+ *
35
+ * One entry = one dialog listing every item (a single download is an
36
+ * items array of one; a batched plugin call is one entry for the whole
37
+ * batch).
23
38
  */
24
39
  export type DownloadConsentEntry = {
25
40
  readonly ticketId: string
26
41
  readonly pluginId: string
27
42
  readonly pluginName: string
28
- readonly url: string
29
- readonly dest: string
30
- readonly sizeBytes?: number
31
- readonly reason?: string
43
+ readonly items: readonly DownloadConsentItem[]
32
44
  }
33
45
 
34
46
  type State = {
@@ -1,3 +1,4 @@
1
+ import { PLUGIN_ASSET_BATCH_MAX_ITEMS } from "@hoardodile/sdk-types/plugin-asset-limits"
1
2
  import { anchorData as anchorDataSchema } from "@hoardodile/sdk-types/schema"
2
3
  import type { PluginRequests } from "@hoardodile/sdk-web"
3
4
  import { pluginMethods } from "@hoardodile/sdk-web"
@@ -17,6 +18,17 @@ import { z } from "zod"
17
18
  */
18
19
  export const anchorData = anchorDataSchema
19
20
 
21
+ /** One download request (a batch is an array of these). */
22
+ const downloadItem = z.object({
23
+ url: z.string().min(1).max(2048),
24
+ dest: z.string().min(1).max(256),
25
+ sha256: z
26
+ .string()
27
+ .regex(/^[0-9a-f]{64}$/)
28
+ .optional(),
29
+ reason: z.string().max(200).optional(),
30
+ })
31
+
20
32
  export const requestSchemas = {
21
33
  [pluginMethods.readFile]: z.object({
22
34
  path: z.string().min(1),
@@ -57,16 +69,13 @@ export const requestSchemas = {
57
69
  target: z.enum(["resource", "resources", "messages", "danmaku"]),
58
70
  }),
59
71
  // The URL is length-capped but not URI-validated here: the host parses
60
- // it server-side and answers with a machine-readable POLICY error.
61
- [pluginMethods.download]: z.object({
62
- url: z.string().min(1).max(2048),
63
- dest: z.string().min(1).max(256),
64
- sha256: z
65
- .string()
66
- .regex(/^[0-9a-f]{64}$/)
67
- .optional(),
68
- reason: z.string().max(200).optional(),
69
- }),
72
+ // it server-side and answers with a machine-readable POLICY error. A
73
+ // single request or a batch (one consent dialog listing every item,
74
+ // all-or-nothing) — capped so a burst cannot stack unbounded tickets.
75
+ [pluginMethods.download]: z.union([
76
+ downloadItem,
77
+ z.array(downloadItem).min(1).max(PLUGIN_ASSET_BATCH_MAX_ITEMS),
78
+ ]),
70
79
  [pluginMethods.deleteAsset]: z.object({
71
80
  path: z.string().min(1).max(256),
72
81
  }),
package/src/mock/host.ts CHANGED
@@ -90,8 +90,8 @@ export type MockHostOptions = {
90
90
  */
91
91
  export type PluginAssetVaultMock = {
92
92
  readonly download: (
93
- request: PluginDownloadRequest,
94
- ) => Promise<PluginDownloadResult>
93
+ request: PluginDownloadRequest | readonly PluginDownloadRequest[],
94
+ ) => Promise<PluginDownloadResult | readonly PluginDownloadResult[]>
95
95
  readonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>
96
96
  }
97
97