@kehto/services 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -373,7 +373,7 @@ function createRelayPoolService(options) {
373
373
  send({ type: "relay.eose", subId });
374
374
  }
375
375
  }, EOSE_FALLBACK_MS);
376
- const handle = options.subscribe(filters, (item) => {
376
+ const handle = options.subscribe(filters, (item, observedRelayUrls) => {
377
377
  if (item === "EOSE") {
378
378
  clearTimeout(eoseTimer);
379
379
  if (!eoseSent) {
@@ -382,7 +382,11 @@ function createRelayPoolService(options) {
382
382
  }
383
383
  return;
384
384
  }
385
- send({ type: "relay.event", subId, result: createRelayEventResultWithHints(item, relayUrls2) });
385
+ send({
386
+ type: "relay.event",
387
+ subId,
388
+ result: createRelayEventResultWithHints(item, observedRelayUrls ?? relayUrls2)
389
+ });
386
390
  }, relayUrls2);
387
391
  tracked.set(subKey, { handle, eoseTimer });
388
392
  return;
@@ -674,14 +678,14 @@ function createCoordinatedRelay(options) {
674
678
  }, timeoutMs);
675
679
  const relayHint = typeof relayMessage.relay === "string" && relayMessage.relay.length > 0 ? relayMessage.relay : void 0;
676
680
  const relayUrls2 = relayHint ? [relayHint] : options.relayPool.selectRelayTier(filters);
677
- tracked.relayHandle = options.relayPool.subscribe(filters, (item) => {
681
+ tracked.relayHandle = options.relayPool.subscribe(filters, (item, observedRelayUrls) => {
678
682
  if (item === "EOSE") {
679
683
  clearTimeout(tracked.eoseTimer);
680
684
  tracked.relayEose = true;
681
685
  maybeSendEose(subKey, subId, send);
682
686
  return;
683
687
  }
684
- deliver2(item, relayUrls2);
688
+ deliver2(item, observedRelayUrls ?? relayUrls2);
685
689
  if (cacheAvailable) {
686
690
  try {
687
691
  options.cache.store(item);
@@ -1498,47 +1502,117 @@ function createMediaService(options = {}) {
1498
1502
 
1499
1503
  // src/notify-service.ts
1500
1504
  var NOTIFY_SERVICE_VERSION = "1.0.0";
1505
+ function errorText(error) {
1506
+ return error instanceof Error ? error.message : String(error);
1507
+ }
1508
+ function forget(result2, onError) {
1509
+ void Promise.resolve(result2).catch((error) => onError?.(error));
1510
+ }
1501
1511
  function createNotifyService(options = {}) {
1502
1512
  let counter = 0;
1503
- const gen = options.generateId ?? (() => {
1504
- counter += 1;
1505
- return `shell-${counter}`;
1506
- });
1507
- const defaultGrant = options.defaultGrant ?? true;
1513
+ const generateId2 = options.generateId ?? (() => `shell-${++counter}`);
1514
+ const active = /* @__PURE__ */ new Map();
1515
+ const controlsSent = /* @__PURE__ */ new Set();
1508
1516
  const descriptor = {
1509
1517
  name: "notify",
1510
1518
  version: NOTIFY_SERVICE_VERSION,
1511
- description: "NIP-5D notify NAP reference handler (stub)"
1519
+ description: "NAP-NOTIFY host-backed notification handler"
1520
+ };
1521
+ const pushControls = (windowId, send) => {
1522
+ if (!options.controls || controlsSent.has(windowId)) return;
1523
+ controlsSent.add(windowId);
1524
+ const message = { type: "notify.controls", controls: [...options.controls] };
1525
+ send(message);
1526
+ };
1527
+ const sendNotification = async (windowId, message, send) => {
1528
+ if (!options.present) {
1529
+ const result2 = {
1530
+ type: "notify.send.result",
1531
+ id: message.id,
1532
+ error: "notification presentation unavailable"
1533
+ };
1534
+ send(result2);
1535
+ return;
1536
+ }
1537
+ const notificationId = generateId2();
1538
+ const windowNotifications = active.get(windowId) ?? /* @__PURE__ */ new Set();
1539
+ windowNotifications.add(notificationId);
1540
+ active.set(windowId, windowNotifications);
1541
+ const ownsWindow = () => active.get(windowId) === windowNotifications;
1542
+ const ownsNotification = () => ownsWindow() && windowNotifications.has(notificationId);
1543
+ const emit = (interaction) => {
1544
+ if (!ownsNotification()) return;
1545
+ if (interaction.type === "notify.dismissed") windowNotifications.delete(notificationId);
1546
+ send(interaction);
1547
+ };
1548
+ try {
1549
+ await options.present({ windowId, notificationId, message, emit });
1550
+ if (!ownsWindow()) {
1551
+ if (options.dismiss) forget(options.dismiss(windowId, notificationId), options.onError);
1552
+ return;
1553
+ }
1554
+ const result2 = {
1555
+ type: "notify.send.result",
1556
+ id: message.id,
1557
+ notificationId
1558
+ };
1559
+ send(result2);
1560
+ } catch (error) {
1561
+ if (!ownsWindow()) return;
1562
+ windowNotifications.delete(notificationId);
1563
+ if (windowNotifications.size === 0) active.delete(windowId);
1564
+ const result2 = {
1565
+ type: "notify.send.result",
1566
+ id: message.id,
1567
+ error: errorText(error)
1568
+ };
1569
+ send(result2);
1570
+ }
1571
+ };
1572
+ const requestPermission = async (windowId, message, send) => {
1573
+ let granted = false;
1574
+ try {
1575
+ granted = options.requestPermission ? await options.requestPermission(windowId, message.channel) : false;
1576
+ } catch (error) {
1577
+ options.onError?.(error);
1578
+ }
1579
+ const result2 = {
1580
+ type: "notify.permission.result",
1581
+ id: message.id,
1582
+ granted
1583
+ };
1584
+ send(result2);
1512
1585
  };
1513
1586
  return {
1514
1587
  descriptor,
1515
1588
  handleMessage(windowId, message, send) {
1589
+ pushControls(windowId, send);
1516
1590
  switch (message.type) {
1517
- case "notify.send": {
1518
- const m = message;
1519
- options.onSend?.(windowId, m);
1520
- const result2 = {
1521
- type: "notify.send.result",
1522
- id: m.id,
1523
- notificationId: gen()
1524
- };
1525
- send(result2);
1591
+ case "notify.send":
1592
+ void sendNotification(windowId, message, send);
1593
+ return;
1594
+ case "notify.dismiss": {
1595
+ const notificationId = message.notificationId;
1596
+ const windowNotifications = active.get(windowId);
1597
+ if (!windowNotifications?.delete(notificationId)) return;
1598
+ if (options.dismiss) forget(options.dismiss(windowId, notificationId), options.onError);
1599
+ return;
1600
+ }
1601
+ case "notify.badge": {
1602
+ if (options.setBadge) {
1603
+ const count = Math.max(0, Math.floor(message.count));
1604
+ forget(options.setBadge(windowId, count), options.onError);
1605
+ }
1526
1606
  return;
1527
1607
  }
1528
- case "notify.dismiss":
1529
- case "notify.badge":
1530
1608
  case "notify.channel.register":
1609
+ if (options.registerChannel) {
1610
+ forget(options.registerChannel(windowId, message), options.onError);
1611
+ }
1531
1612
  return;
1532
- case "notify.permission.request": {
1533
- const m = message;
1534
- const result2 = {
1535
- type: "notify.permission.result",
1536
- id: m.id,
1537
- granted: defaultGrant
1538
- };
1539
- send(result2);
1613
+ case "notify.permission.request":
1614
+ void requestPermission(windowId, message, send);
1540
1615
  return;
1541
- }
1542
1616
  default: {
1543
1617
  const id = message.id ?? "";
1544
1618
  send({
@@ -1549,7 +1623,16 @@ function createNotifyService(options = {}) {
1549
1623
  }
1550
1624
  }
1551
1625
  },
1552
- onWindowDestroyed(_windowId) {
1626
+ onWindowDestroyed(windowId) {
1627
+ const windowNotifications = active.get(windowId);
1628
+ active.delete(windowId);
1629
+ controlsSent.delete(windowId);
1630
+ if (options.dismiss && windowNotifications) {
1631
+ for (const notificationId of windowNotifications) {
1632
+ forget(options.dismiss(windowId, notificationId), options.onError);
1633
+ }
1634
+ }
1635
+ if (options.destroyWindow) forget(options.destroyWindow(windowId), options.onError);
1553
1636
  }
1554
1637
  };
1555
1638
  }
@@ -1610,157 +1693,584 @@ function createThemeService(options = {}) {
1610
1693
  return { handler, publishTheme, getCurrentTheme };
1611
1694
  }
1612
1695
 
1613
- // src/config-service.ts
1614
- var CONFIG_SERVICE_VERSION = "1.0.0";
1615
- function validateCoreSubset(schema) {
1616
- if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
1696
+ // src/config-schema.ts
1697
+ var CONFIG_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
1698
+ var CONFIG_SCHEMA_KEYS = /* @__PURE__ */ new Set([
1699
+ "$schema",
1700
+ "$version",
1701
+ "type",
1702
+ "properties",
1703
+ "required",
1704
+ "items",
1705
+ "additionalProperties",
1706
+ "default",
1707
+ "title",
1708
+ "description",
1709
+ "enum",
1710
+ "enumDescriptions",
1711
+ "minimum",
1712
+ "maximum",
1713
+ "minLength",
1714
+ "maxLength",
1715
+ "minItems",
1716
+ "maxItems",
1717
+ "format",
1718
+ "deprecationMessage",
1719
+ "markdownDescription"
1720
+ ]);
1721
+ var FORBIDDEN_SCHEMA_KEYS = /* @__PURE__ */ new Set([
1722
+ "$ref",
1723
+ "$defs",
1724
+ "definitions",
1725
+ "pattern",
1726
+ "patternProperties",
1727
+ "propertyNames",
1728
+ "dependencies",
1729
+ "dependentSchemas",
1730
+ "unevaluatedProperties",
1731
+ "oneOf",
1732
+ "anyOf",
1733
+ "allOf",
1734
+ "not",
1735
+ "if",
1736
+ "then",
1737
+ "else"
1738
+ ]);
1739
+ function validateConfigSchema(schema) {
1740
+ if (!isRecord(schema)) {
1617
1741
  return { ok: false, code: "invalid-schema", error: "schema root must be an object" };
1618
1742
  }
1619
- const s = schema;
1620
- if ("$ref" in s) {
1621
- return { ok: false, code: "ref-not-allowed", error: "$ref is not permitted in the Core Subset" };
1743
+ if (schema.type !== "object") {
1744
+ return { ok: false, code: "invalid-schema", error: 'schema root must have type: "object"' };
1622
1745
  }
1623
- if ("pattern" in s) {
1624
- return {
1625
- ok: false,
1626
- code: "pattern-not-allowed",
1627
- error: "pattern is not permitted in the Core Subset"
1628
- };
1746
+ if (schema.$schema !== void 0) {
1747
+ if (typeof schema.$schema !== "string" || !isSupportedSchemaDraft(schema.$schema)) {
1748
+ return { ok: false, code: "unsupported-draft", error: "schema draft is not supported" };
1749
+ }
1629
1750
  }
1630
- if ("oneOf" in s || "anyOf" in s || "allOf" in s || "not" in s) {
1631
- return {
1632
- ok: false,
1633
- code: "invalid-schema",
1634
- error: "oneOf/anyOf/allOf/not are not permitted in the Core Subset"
1635
- };
1751
+ if (schema.$version !== void 0 && (!Number.isSafeInteger(schema.$version) || Number(schema.$version) < 0)) {
1752
+ return { ok: false, code: "invalid-schema", error: "$version must be a non-negative integer" };
1636
1753
  }
1637
- if ("if" in s || "then" in s || "else" in s) {
1638
- return {
1639
- ok: false,
1640
- code: "invalid-schema",
1641
- error: "if/then/else are not permitted in the Core Subset"
1642
- };
1754
+ return validateSchemaNode(schema, 0, "$");
1755
+ }
1756
+ function validateSchemaNode(schema, depth, path) {
1757
+ if (schema.type === "object" && depth > 4) {
1758
+ return { ok: false, code: "schema-too-deep", error: `${path} exceeds the four-level object depth limit` };
1643
1759
  }
1644
- if (s.type !== "object") {
1645
- return { ok: false, code: "invalid-schema", error: 'schema root must have type: "object"' };
1760
+ for (const key of Object.keys(schema)) {
1761
+ if (FORBIDDEN_SCHEMA_KEYS.has(key)) {
1762
+ const code = key === "$ref" || key === "$defs" || key === "definitions" ? "ref-not-allowed" : key === "pattern" ? "pattern-not-allowed" : "invalid-schema";
1763
+ return { ok: false, code, error: `${key} is not permitted in the Core Subset at ${path}` };
1764
+ }
1765
+ if (!CONFIG_SCHEMA_KEYS.has(key) && !key.startsWith("x-napplet-")) {
1766
+ return { ok: false, code: "invalid-schema", error: `${key} is outside the NAP-CONFIG Core Subset at ${path}` };
1767
+ }
1768
+ if (path !== "$" && (key === "$schema" || key === "$version")) {
1769
+ return { ok: false, code: "invalid-schema", error: `${key} is only valid at the schema root` };
1770
+ }
1646
1771
  }
1647
- const props = s.properties;
1648
- if (props !== void 0 && (typeof props !== "object" || props === null)) {
1649
- return { ok: false, code: "invalid-schema", error: "properties must be an object" };
1772
+ if (typeof schema.type !== "string" || !CONFIG_TYPES.has(schema.type)) {
1773
+ return { ok: false, code: "invalid-schema", error: `${path} has an unsupported or missing type` };
1650
1774
  }
1651
- if (props) {
1652
- for (const [key, val] of Object.entries(props)) {
1653
- if (typeof val !== "object" || val === null) {
1654
- return {
1655
- ok: false,
1656
- code: "invalid-schema",
1657
- error: `property "${key}" must be an object schema`
1658
- };
1659
- }
1660
- const pv = val;
1661
- const ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "array", "object"]);
1662
- if (pv.type !== void 0 && !ALLOWED_TYPES.has(pv.type)) {
1663
- return {
1664
- ok: false,
1665
- code: "invalid-schema",
1666
- error: `property "${key}" must have type: string|number|boolean|array|object`
1667
- };
1668
- }
1775
+ if (schema["x-napplet-secret"] === true && Object.hasOwn(schema, "default")) {
1776
+ return { ok: false, code: "secret-with-default", error: `${path} combines x-napplet-secret with default` };
1777
+ }
1778
+ const annotations = validateAnnotations(schema, path);
1779
+ if (!annotations.ok) return annotations;
1780
+ if (schema.type === "object") return validateObjectSchema(schema, depth, path);
1781
+ if (schema.type === "array") {
1782
+ if (!isRecord(schema.items)) {
1783
+ return { ok: false, code: "invalid-schema", error: `${path}.items must be one homogeneous schema` };
1784
+ }
1785
+ const itemType = schema.items.type;
1786
+ if (itemType === "object" || itemType === "array") {
1787
+ return { ok: false, code: "invalid-schema", error: `${path} arrays may contain primitives only` };
1788
+ }
1789
+ const constraints2 = validateScalarConstraints(schema, path);
1790
+ if (!constraints2.ok) return constraints2;
1791
+ const itemValidation = validateSchemaNode(schema.items, depth, `${path}[]`);
1792
+ if (!itemValidation.ok) return itemValidation;
1793
+ return validateDefault(schema, path);
1794
+ }
1795
+ const constraints = validateScalarConstraints(schema, path);
1796
+ if (!constraints.ok) return constraints;
1797
+ return validateDefault(schema, path);
1798
+ }
1799
+ function validateObjectSchema(schema, depth, path) {
1800
+ const constraints = validateScalarConstraints(schema, path);
1801
+ if (!constraints.ok) return constraints;
1802
+ if (schema.properties !== void 0 && !isRecord(schema.properties)) {
1803
+ return { ok: false, code: "invalid-schema", error: `${path}.properties must be an object` };
1804
+ }
1805
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string") || new Set(schema.required).size !== schema.required.length)) {
1806
+ return { ok: false, code: "invalid-schema", error: `${path}.required must contain unique strings` };
1807
+ }
1808
+ if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties !== "boolean") {
1809
+ return { ok: false, code: "invalid-schema", error: `${path}.additionalProperties must be boolean` };
1810
+ }
1811
+ const properties = schema.properties ?? {};
1812
+ if (Array.isArray(schema.required) && schema.required.some((key) => !Object.hasOwn(properties, key))) {
1813
+ return { ok: false, code: "invalid-schema", error: `${path}.required references an undeclared property` };
1814
+ }
1815
+ for (const [key, child] of Object.entries(properties)) {
1816
+ if (!isRecord(child)) {
1817
+ return { ok: false, code: "invalid-schema", error: `${path}.${key} must be an object schema` };
1818
+ }
1819
+ const validation = validateSchemaNode(child, child.type === "object" ? depth + 1 : depth, `${path}.${key}`);
1820
+ if (!validation.ok) return validation;
1821
+ }
1822
+ return validateDefault(schema, path);
1823
+ }
1824
+ function validateScalarConstraints(schema, path) {
1825
+ const numericKeys = schema.type === "number" || schema.type === "integer" ? ["minimum", "maximum"] : [];
1826
+ const countKeys = schema.type === "string" ? ["minLength", "maxLength"] : schema.type === "array" ? ["minItems", "maxItems"] : [];
1827
+ for (const key of ["minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems"]) {
1828
+ if (schema[key] !== void 0 && !numericKeys.includes(key) && !countKeys.includes(key)) {
1829
+ return { ok: false, code: "invalid-schema", error: `${path}.${key} does not apply to ${String(schema.type)}` };
1830
+ }
1831
+ }
1832
+ for (const key of numericKeys) {
1833
+ if (schema[key] !== void 0 && (typeof schema[key] !== "number" || !Number.isFinite(schema[key]))) {
1834
+ return { ok: false, code: "invalid-schema", error: `${path}.${key} must be a finite number` };
1835
+ }
1836
+ }
1837
+ for (const key of countKeys) {
1838
+ if (schema[key] !== void 0 && (!Number.isSafeInteger(schema[key]) || Number(schema[key]) < 0)) {
1839
+ return { ok: false, code: "invalid-schema", error: `${path}.${key} must be a non-negative integer` };
1840
+ }
1841
+ }
1842
+ if (typeof schema.minimum === "number" && typeof schema.maximum === "number" && schema.minimum > schema.maximum) {
1843
+ return { ok: false, code: "invalid-schema", error: `${path}.minimum cannot exceed maximum` };
1844
+ }
1845
+ if (typeof schema.minLength === "number" && typeof schema.maxLength === "number" && schema.minLength > schema.maxLength) {
1846
+ return { ok: false, code: "invalid-schema", error: `${path}.minLength cannot exceed maxLength` };
1847
+ }
1848
+ if (typeof schema.minItems === "number" && typeof schema.maxItems === "number" && schema.minItems > schema.maxItems) {
1849
+ return { ok: false, code: "invalid-schema", error: `${path}.minItems cannot exceed maxItems` };
1850
+ }
1851
+ if (schema.enum !== void 0) {
1852
+ if (!Array.isArray(schema.enum) || schema.enum.length === 0) {
1853
+ return { ok: false, code: "invalid-schema", error: `${path}.enum must be a non-empty array` };
1854
+ }
1855
+ if (schema.enum.some((value) => !isPrimitiveForType(value, String(schema.type)))) {
1856
+ return { ok: false, code: "invalid-schema", error: `${path}.enum contains a value outside its declared type` };
1857
+ }
1858
+ if (new Set(schema.enum.map((value) => JSON.stringify(value))).size !== schema.enum.length) {
1859
+ return { ok: false, code: "invalid-schema", error: `${path}.enum values must be unique` };
1669
1860
  }
1670
1861
  }
1671
1862
  return { ok: true };
1672
1863
  }
1673
- function createConfigService(options) {
1674
- const subscribers = /* @__PURE__ */ new Map();
1675
- const descriptor = {
1676
- name: "config",
1677
- version: CONFIG_SERVICE_VERSION,
1678
- description: "NAP-CONFIG reference service \u2014 shell-writes, napplet-reads configuration"
1679
- };
1680
- const handler = {
1681
- descriptor,
1682
- handleMessage(windowId, message, send) {
1683
- switch (message.type) {
1684
- case "config.get": {
1685
- const m = message;
1686
- const reply = {
1687
- type: "config.values",
1688
- id: m.id,
1689
- values: options.getValues()
1690
- };
1691
- send(reply);
1692
- return;
1693
- }
1694
- case "config.subscribe": {
1695
- subscribers.set(windowId, send);
1696
- const push = {
1697
- type: "config.values",
1698
- values: options.getValues()
1699
- };
1700
- send(push);
1701
- options.onSubscribe?.(windowId);
1702
- return;
1703
- }
1704
- case "config.unsubscribe": {
1705
- subscribers.delete(windowId);
1706
- options.onUnsubscribe?.(windowId);
1707
- return;
1708
- }
1709
- case "config.registerSchema": {
1710
- const m = message;
1711
- const validation = options.registerSchema ? options.registerSchema(windowId, m.schema, m.version) : validateCoreSubset(m.schema);
1712
- const result2 = validation.ok ? { type: "config.registerSchema.result", id: m.id, ok: true } : {
1713
- type: "config.registerSchema.result",
1714
- id: m.id,
1715
- ok: false,
1716
- code: validation.code,
1717
- error: validation.error
1718
- };
1719
- send(result2);
1720
- return;
1721
- }
1722
- case "config.openSettings": {
1723
- const m = message;
1724
- options.openSettings?.(windowId, m.section);
1725
- return;
1726
- }
1727
- default:
1728
- return;
1864
+ function validateAnnotations(schema, path) {
1865
+ for (const key of ["title", "description", "format", "x-napplet-section", "deprecationMessage", "markdownDescription"]) {
1866
+ if (schema[key] !== void 0 && typeof schema[key] !== "string") {
1867
+ return { ok: false, code: "invalid-schema", error: `${path}.${key} must be a string` };
1868
+ }
1869
+ }
1870
+ if (schema["x-napplet-secret"] !== void 0 && typeof schema["x-napplet-secret"] !== "boolean") {
1871
+ return { ok: false, code: "invalid-schema", error: `${path}.x-napplet-secret must be boolean` };
1872
+ }
1873
+ if (schema["x-napplet-secret"] === true && schema.type !== "string") {
1874
+ return { ok: false, code: "invalid-schema", error: `${path}.x-napplet-secret applies only to strings` };
1875
+ }
1876
+ if (schema["x-napplet-order"] !== void 0 && (typeof schema["x-napplet-order"] !== "number" || !Number.isFinite(schema["x-napplet-order"]) || schema["x-napplet-order"] < 0)) {
1877
+ return { ok: false, code: "invalid-schema", error: `${path}.x-napplet-order must be a non-negative number` };
1878
+ }
1879
+ if (schema.enumDescriptions !== void 0 && (!Array.isArray(schema.enumDescriptions) || schema.enumDescriptions.some((value) => typeof value !== "string") || !Array.isArray(schema.enum) || schema.enumDescriptions.length !== schema.enum.length)) {
1880
+ return { ok: false, code: "invalid-schema", error: `${path}.enumDescriptions must parallel enum` };
1881
+ }
1882
+ return { ok: true };
1883
+ }
1884
+ function validateDefault(schema, path) {
1885
+ if (!Object.hasOwn(schema, "default")) return { ok: true };
1886
+ if (!configValueConforms(schema, schema.default, path === "$")) {
1887
+ return { ok: false, code: "invalid-schema", error: `${path}.default does not validate against its schema` };
1888
+ }
1889
+ return { ok: true };
1890
+ }
1891
+ function configValuesConform(schema, value) {
1892
+ return configValueConforms(schema, value, true);
1893
+ }
1894
+ function configValueConforms(schema, value, topLevel) {
1895
+ if (Array.isArray(schema.enum) && !schema.enum.some((item) => Object.is(item, value))) return false;
1896
+ if (schema.type === "string") {
1897
+ return typeof value === "string" && withinLength(value.length, schema.minLength, schema.maxLength);
1898
+ }
1899
+ if (schema.type === "number") {
1900
+ return typeof value === "number" && Number.isFinite(value) && withinLength(value, schema.minimum, schema.maximum);
1901
+ }
1902
+ if (schema.type === "integer") {
1903
+ return Number.isSafeInteger(value) && withinLength(value, schema.minimum, schema.maximum);
1904
+ }
1905
+ if (schema.type === "boolean") return typeof value === "boolean";
1906
+ if (schema.type === "array") {
1907
+ return Array.isArray(value) && isRecord(schema.items) && withinLength(value.length, schema.minItems, schema.maxItems) && value.every((item) => configValueConforms(schema.items, item, false));
1908
+ }
1909
+ if (schema.type === "object") {
1910
+ if (!isRecord(value)) return false;
1911
+ const properties = isRecord(schema.properties) ? schema.properties : {};
1912
+ if (Array.isArray(schema.required) && schema.required.some((key) => typeof key === "string" && !Object.hasOwn(value, key))) {
1913
+ return false;
1914
+ }
1915
+ return Object.entries(value).every(([key, child]) => {
1916
+ const childSchema = properties[key];
1917
+ return isRecord(childSchema) ? configValueConforms(childSchema, child, false) : allowsAdditionalProperties(schema, topLevel) && cloneJsonConfigValue(child) !== void 0;
1918
+ });
1919
+ }
1920
+ return false;
1921
+ }
1922
+ function allowsAdditionalProperties(schema, topLevel) {
1923
+ return schema.additionalProperties === true || !topLevel && schema.additionalProperties === void 0;
1924
+ }
1925
+ function isPrimitiveForType(value, type) {
1926
+ if (type === "string") return typeof value === "string";
1927
+ if (type === "number") return typeof value === "number" && Number.isFinite(value);
1928
+ if (type === "integer") return Number.isSafeInteger(value);
1929
+ if (type === "boolean") return typeof value === "boolean";
1930
+ return false;
1931
+ }
1932
+ function isSupportedSchemaDraft(value) {
1933
+ return /^https?:\/\/json-schema\.org\/(?:draft-0?7\/schema#?|draft\/2019-09\/schema#?|draft\/2020-12\/schema#?)$/u.test(value);
1934
+ }
1935
+ function isRecord(value) {
1936
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1937
+ }
1938
+ function resolveConfigValues(schema, values) {
1939
+ return resolveObjectValues(schema, isRecord(values) ? values : {}, {}, true);
1940
+ }
1941
+ function resolveObjectValues(schema, input, inheritedDefault, topLevel) {
1942
+ const output = {};
1943
+ const ownDefault = isRecord(schema.default) ? schema.default : {};
1944
+ const defaults = { ...inheritedDefault, ...ownDefault };
1945
+ for (const [key, childValue] of Object.entries(schema.properties ?? {})) {
1946
+ if (!isRecord(childValue)) continue;
1947
+ const candidate = Object.hasOwn(input, key) ? input[key] : defaults[key];
1948
+ const resolved = resolveConfigValue(childValue, candidate);
1949
+ if (resolved !== void 0) setConfigValue(output, key, resolved);
1950
+ }
1951
+ if (allowsAdditionalProperties(schema, topLevel)) {
1952
+ const properties = isRecord(schema.properties) ? schema.properties : {};
1953
+ for (const [key, candidate] of Object.entries({ ...defaults, ...input })) {
1954
+ if (Object.hasOwn(properties, key)) continue;
1955
+ const resolved = cloneJsonConfigValue(candidate);
1956
+ if (resolved !== void 0) setConfigValue(output, key, resolved);
1957
+ }
1958
+ }
1959
+ return output;
1960
+ }
1961
+ function resolveConfigValue(schema, candidate) {
1962
+ const value = candidate === void 0 ? schema.default : candidate;
1963
+ if (value === void 0) return void 0;
1964
+ const resolved = resolveTypedConfigValue(schema, value);
1965
+ if (resolved !== void 0) return resolved;
1966
+ return candidate !== void 0 && schema.default !== void 0 ? resolveTypedConfigValue(schema, schema.default) : void 0;
1967
+ }
1968
+ function resolveTypedConfigValue(schema, value) {
1969
+ if (schema.enum && Array.isArray(schema.enum) && !schema.enum.some((item) => Object.is(item, value))) return void 0;
1970
+ switch (schema.type) {
1971
+ case "object": {
1972
+ if (!isRecord(value)) return void 0;
1973
+ const resolved = resolveObjectValues(schema, value, {}, false);
1974
+ return Array.isArray(schema.required) && schema.required.some((key) => typeof key === "string" && !Object.hasOwn(resolved, key)) ? void 0 : resolved;
1975
+ }
1976
+ case "array": {
1977
+ if (!Array.isArray(value) || !isRecord(schema.items) || !withinLength(value.length, schema.minItems, schema.maxItems)) {
1978
+ return void 0;
1729
1979
  }
1730
- },
1731
- onWindowDestroyed(windowId) {
1732
- subscribers.delete(windowId);
1980
+ const resolved = value.map((item) => resolveConfigValue(schema.items, item));
1981
+ if (resolved.some((item) => item === void 0)) return void 0;
1982
+ return resolved;
1733
1983
  }
1734
- };
1735
- function publishValues(values) {
1736
- const envelope = {
1984
+ case "string":
1985
+ return typeof value === "string" && withinLength(value.length, schema.minLength, schema.maxLength) ? value : void 0;
1986
+ case "number":
1987
+ return typeof value === "number" && Number.isFinite(value) && withinLength(value, schema.minimum, schema.maximum) ? value : void 0;
1988
+ case "integer":
1989
+ return Number.isSafeInteger(value) && withinLength(value, schema.minimum, schema.maximum) ? value : void 0;
1990
+ case "boolean":
1991
+ return typeof value === "boolean" ? value : void 0;
1992
+ default:
1993
+ return void 0;
1994
+ }
1995
+ }
1996
+ function cloneJsonConfigValue(value, seen = /* @__PURE__ */ new Set()) {
1997
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
1998
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1999
+ if (typeof value !== "object" || seen.has(value)) return void 0;
2000
+ seen.add(value);
2001
+ try {
2002
+ if (Array.isArray(value)) {
2003
+ const output = value.map((item) => cloneJsonConfigValue(item, seen));
2004
+ return output.some((item) => item === void 0) ? void 0 : output;
2005
+ }
2006
+ const entries = [];
2007
+ for (const [key, child] of Object.entries(value)) {
2008
+ const resolved = cloneJsonConfigValue(child, seen);
2009
+ if (resolved === void 0) return void 0;
2010
+ entries.push([key, resolved]);
2011
+ }
2012
+ return Object.fromEntries(entries);
2013
+ } finally {
2014
+ seen.delete(value);
2015
+ }
2016
+ }
2017
+ function setConfigValue(output, key, value) {
2018
+ Object.defineProperty(output, key, { value, enumerable: true, configurable: true, writable: true });
2019
+ }
2020
+ function withinLength(value, minimum, maximum) {
2021
+ return (typeof minimum !== "number" || value >= minimum) && (typeof maximum !== "number" || value <= maximum);
2022
+ }
2023
+
2024
+ // src/config-service.ts
2025
+ var CONFIG_SERVICE_VERSION = "1.0.0";
2026
+ function createConfigService(options) {
2027
+ return new ConfigServiceController(options);
2028
+ }
2029
+ var ConfigServiceController = class {
2030
+ constructor(options) {
2031
+ this.options = options;
2032
+ this.handler = {
2033
+ descriptor: {
2034
+ name: "config",
2035
+ version: CONFIG_SERVICE_VERSION,
2036
+ description: "NAP-CONFIG reference service \u2014 shell-writes, napplet-reads configuration"
2037
+ },
2038
+ handleMessage: (windowId, message, send) => this.handleMessage(windowId, message, send),
2039
+ onWindowDestroyed: (windowId) => this.destroyWindow(windowId)
2040
+ };
2041
+ }
2042
+ options;
2043
+ subscribers = /* @__PURE__ */ new Map();
2044
+ schemas = /* @__PURE__ */ new Map();
2045
+ handler;
2046
+ getSchema(windowId) {
2047
+ const schema = this.schemas.get(windowId)?.schema;
2048
+ return schema ? structuredClone(schema) : null;
2049
+ }
2050
+ getValues(windowId) {
2051
+ const schema = this.schemas.get(windowId)?.schema;
2052
+ if (!schema) return null;
2053
+ const values = resolveConfigValues(schema, this.options.getValues(windowId));
2054
+ return configValuesConform(schema, values) ? values : null;
2055
+ }
2056
+ publishValues(values, windowId) {
2057
+ if (windowId) {
2058
+ this.publishWindowValues(windowId, values);
2059
+ return;
2060
+ }
2061
+ for (const target of this.subscribers.keys()) this.publishWindowValues(target, values);
2062
+ }
2063
+ handleMessage(windowId, message, send) {
2064
+ switch (message.type) {
2065
+ case "config.get":
2066
+ this.handleGet(windowId, message, send);
2067
+ return;
2068
+ case "config.subscribe":
2069
+ this.handleSubscribe(windowId, send);
2070
+ return;
2071
+ case "config.unsubscribe":
2072
+ this.subscribers.delete(windowId);
2073
+ this.options.onUnsubscribe?.(windowId);
2074
+ return;
2075
+ case "config.registerSchema":
2076
+ this.handleRegisterSchema(windowId, message, send);
2077
+ return;
2078
+ case "config.openSettings":
2079
+ this.handleOpenSettings(windowId, message, send);
2080
+ return;
2081
+ default:
2082
+ return;
2083
+ }
2084
+ }
2085
+ handleGet(windowId, message, send) {
2086
+ const values = this.readValues(windowId, send);
2087
+ if (values === void 0) return;
2088
+ if (!values) {
2089
+ this.sendValuesUnavailable(send, windowId);
2090
+ return;
2091
+ }
2092
+ const reply = {
1737
2093
  type: "config.values",
2094
+ id: message.id,
1738
2095
  values
1739
2096
  };
1740
- for (const send of subscribers.values()) {
1741
- try {
1742
- send(envelope);
1743
- } catch {
1744
- }
2097
+ send(reply);
2098
+ }
2099
+ handleSubscribe(windowId, send) {
2100
+ this.subscribers.set(windowId, send);
2101
+ const values = this.readValues(windowId, send);
2102
+ if (values === void 0) return;
2103
+ if (!values) {
2104
+ this.sendValuesUnavailable(send, windowId);
2105
+ return;
2106
+ }
2107
+ send({ type: "config.values", values });
2108
+ this.options.onSubscribe?.(windowId);
2109
+ }
2110
+ handleRegisterSchema(windowId, message, send) {
2111
+ const nextVersion = message.version ?? readSchemaVersion(message.schema);
2112
+ let validation = this.validateRegistration(windowId, message, nextVersion);
2113
+ if (validation.ok) validation = this.persistRegisteredSchema(windowId, message.schema, nextVersion);
2114
+ const result2 = validation.ok ? { type: "config.registerSchema.result", id: message.id, ok: true } : {
2115
+ type: "config.registerSchema.result",
2116
+ id: message.id,
2117
+ ok: false,
2118
+ code: validation.code,
2119
+ error: validation.error
2120
+ };
2121
+ send(result2);
2122
+ if (validation.ok) this.publishWindowValues(windowId);
2123
+ }
2124
+ validateRegistration(windowId, message, nextVersion) {
2125
+ let validation = validateConfigSchema(message.schema);
2126
+ if (validation.ok && message.version !== void 0 && (!Number.isSafeInteger(message.version) || message.version < 0)) {
2127
+ validation = { ok: false, code: "invalid-schema", error: "version must be a non-negative integer" };
2128
+ }
2129
+ const priorVersion = this.schemas.get(windowId)?.version;
2130
+ if (validation.ok && priorVersion !== void 0 && nextVersion !== void 0 && nextVersion < priorVersion) {
2131
+ validation = { ok: false, code: "version-conflict", error: "schema version cannot move backwards" };
2132
+ }
2133
+ if (!validation.ok || !this.options.registerSchema) return validation;
2134
+ try {
2135
+ return this.options.registerSchema(windowId, message.schema, message.version);
2136
+ } catch (cause) {
2137
+ return {
2138
+ ok: false,
2139
+ code: "invalid-schema",
2140
+ error: cause instanceof Error ? cause.message : "host schema policy failed"
2141
+ };
2142
+ }
2143
+ }
2144
+ persistRegisteredSchema(windowId, schema, version) {
2145
+ try {
2146
+ const resolved = resolveConfigValues(schema, this.options.getValues(windowId));
2147
+ this.options.saveValues?.(windowId, resolved);
2148
+ this.schemas.set(windowId, {
2149
+ schema: structuredClone(schema),
2150
+ ...version !== void 0 ? { version } : {}
2151
+ });
2152
+ return { ok: true };
2153
+ } catch (cause) {
2154
+ return {
2155
+ ok: false,
2156
+ code: "invalid-schema",
2157
+ error: cause instanceof Error ? cause.message : "host configuration persistence failed"
2158
+ };
2159
+ }
2160
+ }
2161
+ handleOpenSettings(windowId, message, send) {
2162
+ const record = this.schemas.get(windowId);
2163
+ if (!record) {
2164
+ sendSchemaError(send, "no-schema", "no configuration schema is registered");
2165
+ return;
2166
+ }
2167
+ let values;
2168
+ try {
2169
+ values = resolveConfigValues(record.schema, this.options.getValues(windowId));
2170
+ } catch (cause) {
2171
+ sendSchemaError(
2172
+ send,
2173
+ "invalid-schema",
2174
+ cause instanceof Error ? cause.message : "host configuration read failed"
2175
+ );
2176
+ return;
1745
2177
  }
2178
+ const section = message.section && schemaHasSection(record.schema, message.section) ? message.section : void 0;
2179
+ this.options.openSettings?.(windowId, section, {
2180
+ schema: structuredClone(record.schema),
2181
+ values,
2182
+ commit: (next) => this.commitValues(windowId, next)
2183
+ });
2184
+ }
2185
+ commitValues(windowId, values) {
2186
+ const schema = this.schemas.get(windowId)?.schema;
2187
+ if (!schema) throw new Error("no configuration schema is registered");
2188
+ const resolved = resolveConfigValues(schema, values);
2189
+ if (!configValuesConform(schema, resolved)) {
2190
+ throw new Error("required configuration values are missing or invalid");
2191
+ }
2192
+ this.options.saveValues?.(windowId, resolved);
2193
+ this.publishWindowValues(windowId, resolved);
2194
+ return resolved;
2195
+ }
2196
+ publishWindowValues(windowId, supplied) {
2197
+ const send = this.subscribers.get(windowId);
2198
+ const schema = this.schemas.get(windowId)?.schema;
2199
+ if (!send || !schema) return;
2200
+ const values = supplied ? resolveConfigValues(schema, supplied) : this.getValues(windowId);
2201
+ if (!values || !configValuesConform(schema, values)) return;
2202
+ try {
2203
+ send({ type: "config.values", values });
2204
+ } catch {
2205
+ this.subscribers.delete(windowId);
2206
+ }
2207
+ }
2208
+ readValues(windowId, send) {
2209
+ try {
2210
+ return this.getValues(windowId);
2211
+ } catch (cause) {
2212
+ sendSchemaError(
2213
+ send,
2214
+ "invalid-schema",
2215
+ cause instanceof Error ? cause.message : "host configuration read failed"
2216
+ );
2217
+ return void 0;
2218
+ }
2219
+ }
2220
+ sendValuesUnavailable(send, windowId) {
2221
+ if (!this.schemas.has(windowId)) {
2222
+ sendSchemaError(send, "no-schema", "no configuration schema is registered");
2223
+ return;
2224
+ }
2225
+ sendSchemaError(send, "invalid-schema", "configuration requires valid values before delivery");
1746
2226
  }
1747
- return { handler, publishValues };
2227
+ destroyWindow(windowId) {
2228
+ this.subscribers.delete(windowId);
2229
+ this.schemas.delete(windowId);
2230
+ this.options.onWindowDestroyed?.(windowId);
2231
+ }
2232
+ };
2233
+ function sendSchemaError(send, code, error) {
2234
+ send({ type: "config.schemaError", code, error });
2235
+ }
2236
+ function readSchemaVersion(schema) {
2237
+ const version = schema.$version;
2238
+ return Number.isSafeInteger(version) && Number(version) >= 0 ? Number(version) : void 0;
2239
+ }
2240
+ function schemaHasSection(schema, section) {
2241
+ const properties = schema.properties;
2242
+ if (!isRecord2(properties)) return false;
2243
+ for (const child of Object.values(properties)) {
2244
+ if (!isRecord2(child)) continue;
2245
+ if (child["x-napplet-section"] === section) return true;
2246
+ if (child.type === "object" && schemaHasSection(child, section)) return true;
2247
+ }
2248
+ return false;
2249
+ }
2250
+ function isRecord2(value) {
2251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1748
2252
  }
1749
2253
 
1750
2254
  // src/resource-service.ts
1751
2255
  var RESOURCE_SERVICE_VERSION = "1.0.0";
1752
- function arrayBufferToBase64(buf) {
1753
- const bytes = new Uint8Array(buf);
1754
- const CHUNK = 32768;
1755
- let binary = "";
1756
- for (let i = 0; i < bytes.length; i += CHUNK) {
1757
- binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
1758
- }
1759
- return btoa(binary);
1760
- }
2256
+ var ResourceServiceError = class extends Error {
2257
+ /**
2258
+ * @param code - Canonical NAP-RESOURCE error code.
2259
+ * @param message - Diagnostic detail safe to return to the napplet.
2260
+ */
2261
+ constructor(code, message) {
2262
+ super(message);
2263
+ this.code = code;
2264
+ this.name = "ResourceServiceError";
2265
+ }
2266
+ code;
2267
+ };
1761
2268
  var DEFAULT_RESOURCE_INFO = {
1762
- schemes: [{ scheme: "https", enabled: true }]
2269
+ schemes: []
1763
2270
  };
2271
+ function requestKey(windowId, requestId) {
2272
+ return `${windowId}\0${requestId}`;
2273
+ }
1764
2274
  function assertResourceOptions(options) {
1765
2275
  if (typeof options?.fetch !== "function" || typeof options?.isOriginGranted !== "function" || typeof options?.getConnectGrants !== "function" || typeof options?.resolveIdentity !== "function") {
1766
2276
  throw new Error(
@@ -1769,43 +2279,31 @@ function assertResourceOptions(options) {
1769
2279
  }
1770
2280
  }
1771
2281
  function trackRequest(state, requestId, windowId, controller) {
1772
- state.inFlight.set(requestId, { controller, windowId });
2282
+ const key = requestKey(windowId, requestId);
2283
+ state.inFlight.set(key, { controller, windowId, requestId });
1773
2284
  if (!state.perWindow.has(windowId)) {
1774
2285
  state.perWindow.set(windowId, /* @__PURE__ */ new Set());
1775
2286
  }
1776
- state.perWindow.get(windowId).add(requestId);
2287
+ state.perWindow.get(windowId).add(key);
1777
2288
  }
1778
- function untrackRequest(state, requestId) {
1779
- const entry = state.inFlight.get(requestId);
2289
+ function untrackRequest(state, windowId, requestId) {
2290
+ const key = requestKey(windowId, requestId);
2291
+ const entry = state.inFlight.get(key);
1780
2292
  if (entry) {
1781
- state.inFlight.delete(requestId);
1782
- state.perWindow.get(entry.windowId)?.delete(requestId);
2293
+ state.inFlight.delete(key);
2294
+ state.perWindow.get(entry.windowId)?.delete(key);
1783
2295
  }
1784
2296
  }
1785
- function sendResourceError(send, requestId, code, message, error = toResourceError(code), type = "resource.bytes.error") {
2297
+ function sendResourceError(send, requestId, message, error, type = "resource.bytes.error") {
1786
2298
  send({
1787
2299
  type,
1788
2300
  id: requestId,
1789
- requestId,
1790
2301
  error,
1791
- code,
1792
2302
  message
1793
2303
  });
1794
2304
  }
1795
- function sendBytesManyError(send, requestId, code, message, error) {
1796
- sendResourceError(send, requestId, code, message, error, "resource.bytesMany.error");
1797
- }
1798
- function toResourceError(code) {
1799
- switch (code) {
1800
- case "denied":
1801
- return "blocked-by-policy";
1802
- case "invalid-url":
1803
- return "invalid-request";
1804
- case "canceled":
1805
- return "timeout";
1806
- case "network-error":
1807
- return "network-error";
1808
- }
2305
+ function sendBytesManyError(send, requestId, message, error) {
2306
+ sendResourceError(send, requestId, message, error, "resource.bytesMany.error");
1809
2307
  }
1810
2308
  function parseResourceUrl(url) {
1811
2309
  try {
@@ -1814,15 +2312,8 @@ function parseResourceUrl(url) {
1814
2312
  return null;
1815
2313
  }
1816
2314
  }
1817
- function collectResponseHeaders(response) {
1818
- const headers = {};
1819
- response.headers.forEach((value, key) => {
1820
- headers[key] = value;
1821
- });
1822
- return headers;
1823
- }
1824
2315
  function responseMime(response) {
1825
- return response.headers.get("content-type") || "application/octet-stream";
2316
+ return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || "application/octet-stream";
1826
2317
  }
1827
2318
  function responseBlob(buffer, mime) {
1828
2319
  return new Blob([buffer], { type: mime });
@@ -1866,13 +2357,22 @@ function resourceInvalidRequest(url, message) {
1866
2357
  ok: false,
1867
2358
  url,
1868
2359
  error: "invalid-request",
1869
- code: "invalid-url",
1870
2360
  message
1871
2361
  };
1872
2362
  }
1873
- async function fetchResourceItem(options, identity, url, init, signal) {
2363
+ function classifyResourceFailure(error) {
2364
+ return {
2365
+ error: error instanceof ResourceServiceError ? error.code : "network-error",
2366
+ message: error instanceof Error ? error.message : String(error)
2367
+ };
2368
+ }
2369
+ async function fetchResourceItem(options, identity, info, url, signal) {
1874
2370
  const parsedUrl = parseResourceUrl(url);
1875
2371
  if (!parsedUrl) return resourceInvalidRequest(url, `invalid URL: ${url}`);
2372
+ const scheme = parsedUrl.protocol.slice(0, -1).toLowerCase();
2373
+ if (!info.schemes.some((item) => item.enabled && item.scheme.toLowerCase() === scheme)) {
2374
+ return { ok: false, url, error: "unsupported-scheme", message: `scheme ${scheme} is not enabled` };
2375
+ }
1876
2376
  const origin = parsedUrl.origin;
1877
2377
  const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
1878
2378
  if (!options.isOriginGranted(origin, grants)) {
@@ -1880,77 +2380,88 @@ async function fetchResourceItem(options, identity, url, init, signal) {
1880
2380
  ok: false,
1881
2381
  url,
1882
2382
  error: "blocked-by-policy",
1883
- code: "denied",
1884
2383
  message: `origin ${origin} not granted`
1885
2384
  };
1886
2385
  }
1887
2386
  try {
1888
2387
  const response = await options.fetch(url, {
1889
- method: init?.method,
1890
- headers: init?.headers ? { ...init.headers } : void 0,
2388
+ method: "GET",
1891
2389
  signal
1892
2390
  });
1893
2391
  const buffer = await response.arrayBuffer();
1894
- const headers = collectResponseHeaders(response);
2392
+ if (info.maxBytes !== void 0 && buffer.byteLength > info.maxBytes) {
2393
+ return { ok: false, url, error: "too-large", message: `resource exceeds ${info.maxBytes} bytes` };
2394
+ }
1895
2395
  const mime = responseMime(response);
1896
2396
  return {
1897
2397
  ok: true,
1898
2398
  url,
1899
2399
  blob: responseBlob(buffer, mime),
1900
- mime,
1901
- status: response.status,
1902
- headers,
1903
- bodyBase64: arrayBufferToBase64(buffer)
2400
+ mime
1904
2401
  };
1905
2402
  } catch (err2) {
1906
2403
  const isAbort = signal.aborted || err2 instanceof Error && (err2.name === "AbortError" || err2.name === "DOMException");
1907
2404
  return {
1908
2405
  ok: false,
1909
2406
  url,
1910
- error: isAbort ? "timeout" : "network-error",
1911
- code: isAbort ? "canceled" : "network-error",
2407
+ error: isAbort ? "timeout" : err2 instanceof ResourceServiceError ? err2.code : "network-error",
1912
2408
  message: err2 instanceof Error ? err2.message : String(err2)
1913
2409
  };
1914
2410
  }
1915
2411
  }
1916
2412
  async function handleBytes(options, state, windowId, msg, send) {
1917
- const { requestId, url, init } = msg;
2413
+ const { requestId, url } = msg;
1918
2414
  const identity = options.resolveIdentity(windowId);
1919
2415
  if (!identity) {
1920
- sendResourceError(send, requestId, "denied", "napplet identity not resolvable", "blocked-by-policy");
2416
+ sendResourceError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
1921
2417
  return;
1922
2418
  }
1923
2419
  const controller = new AbortController();
1924
2420
  trackRequest(state, requestId, windowId, controller);
1925
2421
  try {
1926
- const item = await fetchResourceItem(options, identity, url, init, controller.signal);
2422
+ const info = await resolveResourceInfo(options, windowId);
2423
+ const item = await fetchResourceItem(options, identity, info, url, controller.signal);
2424
+ if (controller.signal.aborted) return;
1927
2425
  if (!item.ok) {
1928
- sendResourceError(send, requestId, item.code, item.message, item.error);
2426
+ sendResourceError(send, requestId, item.message, item.error);
1929
2427
  return;
1930
2428
  }
1931
2429
  send({
1932
2430
  type: "resource.bytes.result",
1933
2431
  id: requestId,
1934
- requestId,
1935
2432
  blob: item.blob,
1936
- mime: item.mime,
1937
- status: item.status,
1938
- headers: item.headers,
1939
- bodyBase64: item.bodyBase64
2433
+ mime: item.mime
1940
2434
  });
2435
+ } catch (error) {
2436
+ if (!controller.signal.aborted) {
2437
+ const failure = classifyResourceFailure(error);
2438
+ sendResourceError(send, requestId, failure.message, failure.error);
2439
+ }
1941
2440
  } finally {
1942
- untrackRequest(state, requestId);
2441
+ untrackRequest(state, windowId, requestId);
1943
2442
  }
1944
2443
  }
1945
2444
  async function handleBytesMany(options, state, windowId, msg, send) {
1946
- const { requestId, urls, init } = msg;
2445
+ const { requestId, urls } = msg;
1947
2446
  if (!Array.isArray(urls) || urls.length === 0 || urls.some((url) => typeof url !== "string")) {
1948
- sendBytesManyError(send, requestId, "invalid-url", "resource.bytesMany requires a non-empty urls array", "invalid-request");
2447
+ sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty urls array", "invalid-request");
1949
2448
  return;
1950
2449
  }
1951
2450
  const identity = options.resolveIdentity(windowId);
1952
2451
  if (!identity) {
1953
- sendBytesManyError(send, requestId, "denied", "napplet identity not resolvable", "blocked-by-policy");
2452
+ sendBytesManyError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
2453
+ return;
2454
+ }
2455
+ let info;
2456
+ try {
2457
+ info = await resolveResourceInfo(options, windowId);
2458
+ } catch (error) {
2459
+ const failure = classifyResourceFailure(error);
2460
+ sendBytesManyError(send, requestId, failure.message, failure.error);
2461
+ return;
2462
+ }
2463
+ if (info.maxUrls !== void 0 && urls.length > info.maxUrls) {
2464
+ sendBytesManyError(send, requestId, `resource.bytesMany exceeds ${info.maxUrls} URLs`, "too-large");
1954
2465
  return;
1955
2466
  }
1956
2467
  const controller = new AbortController();
@@ -1958,7 +2469,8 @@ async function handleBytesMany(options, state, windowId, msg, send) {
1958
2469
  try {
1959
2470
  const items = [];
1960
2471
  for (const url of urls) {
1961
- const item = await fetchResourceItem(options, identity, url, init, controller.signal);
2472
+ const item = await fetchResourceItem(options, identity, info, url, controller.signal);
2473
+ if (controller.signal.aborted) return;
1962
2474
  if (item.ok) {
1963
2475
  items.push({
1964
2476
  url: item.url,
@@ -1971,7 +2483,6 @@ async function handleBytesMany(options, state, windowId, msg, send) {
1971
2483
  url: item.url,
1972
2484
  ok: false,
1973
2485
  error: item.error,
1974
- code: item.code,
1975
2486
  message: item.message
1976
2487
  });
1977
2488
  }
@@ -1979,15 +2490,19 @@ async function handleBytesMany(options, state, windowId, msg, send) {
1979
2490
  send({
1980
2491
  type: "resource.bytesMany.result",
1981
2492
  id: requestId,
1982
- requestId,
1983
2493
  items
1984
2494
  });
2495
+ } catch (error) {
2496
+ if (!controller.signal.aborted) {
2497
+ const failure = classifyResourceFailure(error);
2498
+ sendBytesManyError(send, requestId, failure.message, failure.error);
2499
+ }
1985
2500
  } finally {
1986
- untrackRequest(state, requestId);
2501
+ untrackRequest(state, windowId, requestId);
1987
2502
  }
1988
2503
  }
1989
- function handleCancel(state, requestId) {
1990
- const entry = state.inFlight.get(requestId);
2504
+ function handleCancel(state, windowId, requestId) {
2505
+ const entry = state.inFlight.get(requestKey(windowId, requestId));
1991
2506
  if (entry) {
1992
2507
  entry.controller.abort();
1993
2508
  }
@@ -1995,11 +2510,11 @@ function handleCancel(state, requestId) {
1995
2510
  function destroyWindowRequests(state, windowId) {
1996
2511
  const requestIds = state.perWindow.get(windowId);
1997
2512
  if (!requestIds) return;
1998
- for (const requestId of requestIds) {
1999
- const entry = state.inFlight.get(requestId);
2513
+ for (const key of requestIds) {
2514
+ const entry = state.inFlight.get(key);
2000
2515
  if (entry) {
2001
2516
  entry.controller.abort();
2002
- state.inFlight.delete(requestId);
2517
+ state.inFlight.delete(key);
2003
2518
  }
2004
2519
  }
2005
2520
  state.perWindow.delete(windowId);
@@ -2022,8 +2537,12 @@ function createResourceService(options) {
2022
2537
  case "resource.bytes": {
2023
2538
  const m = message;
2024
2539
  const requestId = requestIdFromMessage(m);
2025
- if (!requestId || typeof m.url !== "string") return;
2026
- handleBytes(options, state, windowId, { requestId, url: m.url, init: m.init }, send).catch(() => {
2540
+ if (!requestId) return;
2541
+ if (typeof m.url !== "string") {
2542
+ sendResourceError(send, requestId, "resource.bytes requires a URL", "invalid-request");
2543
+ return;
2544
+ }
2545
+ handleBytes(options, state, windowId, { requestId, url: m.url }, send).catch(() => {
2027
2546
  });
2028
2547
  return;
2029
2548
  }
@@ -2039,14 +2558,14 @@ function createResourceService(options) {
2039
2558
  const m = message;
2040
2559
  const requestId = requestIdFromMessage(m);
2041
2560
  if (!requestId) return;
2042
- handleBytesMany(options, state, windowId, { requestId, urls: m.urls ?? [], init: m.init }, send).catch(() => {
2561
+ handleBytesMany(options, state, windowId, { requestId, urls: m.urls ?? [] }, send).catch(() => {
2043
2562
  });
2044
2563
  return;
2045
2564
  }
2046
2565
  case "resource.cancel": {
2047
2566
  const m = message;
2048
2567
  const requestId = requestIdFromMessage(m);
2049
- if (requestId) handleCancel(state, requestId);
2568
+ if (requestId) handleCancel(state, windowId, requestId);
2050
2569
  return;
2051
2570
  }
2052
2571
  default:
@@ -2067,7 +2586,7 @@ var OUTBOX_DESCRIPTOR = {
2067
2586
  version: OUTBOX_SERVICE_VERSION,
2068
2587
  description: "NAP-OUTBOX outbox-aware relay routing \u2014 getEvent/query/subscribe/publish/resolveRelays"
2069
2588
  };
2070
- function isRecord(value) {
2589
+ function isRecord3(value) {
2071
2590
  return typeof value === "object" && value !== null;
2072
2591
  }
2073
2592
  function stringArray(value) {
@@ -2079,7 +2598,7 @@ function uint(value) {
2079
2598
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
2080
2599
  }
2081
2600
  function sanitizeEventOptions(raw) {
2082
- if (!isRecord(raw)) return void 0;
2601
+ if (!isRecord3(raw)) return void 0;
2083
2602
  const out = {};
2084
2603
  if (typeof raw.author === "string") out.author = raw.author;
2085
2604
  const relays = stringArray(raw.relays);
@@ -2089,7 +2608,7 @@ function sanitizeEventOptions(raw) {
2089
2608
  return Object.keys(out).length > 0 ? out : void 0;
2090
2609
  }
2091
2610
  function sanitizeQueryOptions(raw) {
2092
- if (!isRecord(raw)) return void 0;
2611
+ if (!isRecord3(raw)) return void 0;
2093
2612
  const out = {};
2094
2613
  const authors = stringArray(raw.authors);
2095
2614
  if (authors) out.authors = authors;
@@ -2103,7 +2622,7 @@ function sanitizeQueryOptions(raw) {
2103
2622
  }
2104
2623
  var sanitizeSubscribeOptions = sanitizeQueryOptions;
2105
2624
  function sanitizePublishOptions(raw) {
2106
- if (!isRecord(raw)) return void 0;
2625
+ if (!isRecord3(raw)) return void 0;
2107
2626
  const out = {};
2108
2627
  const relays = stringArray(raw.relays);
2109
2628
  if (relays) out.relays = relays;
@@ -2113,7 +2632,7 @@ function sanitizePublishOptions(raw) {
2113
2632
  return Object.keys(out).length > 0 ? out : void 0;
2114
2633
  }
2115
2634
  function sanitizeTarget(raw) {
2116
- if (!isRecord(raw)) return void 0;
2635
+ if (!isRecord3(raw)) return void 0;
2117
2636
  const out = {};
2118
2637
  const authors = stringArray(raw.authors);
2119
2638
  if (authors) out.authors = authors;
@@ -2182,9 +2701,10 @@ function createOutboxService(options) {
2182
2701
  if (!options || typeof options.router !== "object" || options.router === null) {
2183
2702
  throw new Error("createOutboxService: options.router is required");
2184
2703
  }
2185
- const { router } = options;
2704
+ const { router, getQueryRouter } = options;
2705
+ let runtimeContext;
2186
2706
  const subscriptions = /* @__PURE__ */ new Map();
2187
- function handleQuery(msg, send) {
2707
+ function handleQuery(windowId, msg, send) {
2188
2708
  const m = msg;
2189
2709
  const id = m.id ?? "";
2190
2710
  const filters = normalizeFilters(m.filters);
@@ -2192,7 +2712,8 @@ function createOutboxService(options) {
2192
2712
  send({ type: "outbox.query.result", id, events: [], error: "invalid filter" });
2193
2713
  return;
2194
2714
  }
2195
- void router.query(filters, sanitizeQueryOptions(m.options)).then(
2715
+ const queryRouter = getQueryRouter?.(windowId, runtimeContext) ?? router;
2716
+ void queryRouter.query(filters, sanitizeQueryOptions(m.options)).then(
2196
2717
  (result2) => send({
2197
2718
  type: "outbox.query.result",
2198
2719
  id,
@@ -2269,13 +2790,19 @@ function createOutboxService(options) {
2269
2790
  }
2270
2791
  return {
2271
2792
  descriptor: OUTBOX_DESCRIPTOR,
2793
+ onRegistered(context) {
2794
+ runtimeContext = context;
2795
+ },
2796
+ onUnregistered() {
2797
+ runtimeContext = void 0;
2798
+ },
2272
2799
  handleMessage(windowId, message, send) {
2273
2800
  switch (message.type) {
2274
2801
  case "outbox.getEvent":
2275
2802
  handleGetEvent(router, message, send);
2276
2803
  return;
2277
2804
  case "outbox.query":
2278
- handleQuery(message, send);
2805
+ handleQuery(windowId, message, send);
2279
2806
  return;
2280
2807
  case "outbox.subscribe":
2281
2808
  handleSubscribe(windowId, message, send);
@@ -2767,7 +3294,7 @@ function createRelayPoolOutboxRouter(options) {
2767
3294
  if (!Array.isArray(options.fallbackRelays)) {
2768
3295
  throw new Error("createRelayPoolOutboxRouter: options.fallbackRelays is required");
2769
3296
  }
2770
- const verifyEvent = options.verifyEvent;
3297
+ const verifyEvent2 = options.verifyEvent;
2771
3298
  const ctx = {
2772
3299
  relayPool: options.relayPool,
2773
3300
  loadRelayLists: options.loadRelayLists,
@@ -2776,9 +3303,9 @@ function createRelayPoolOutboxRouter(options) {
2776
3303
  isRelayAllowed: options.isRelayAllowed ?? defaultRelayAllowed,
2777
3304
  defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
2778
3305
  async verify(event) {
2779
- if (!verifyEvent) return true;
3306
+ if (!verifyEvent2) return true;
2780
3307
  try {
2781
- return await verifyEvent(event);
3308
+ return await verifyEvent2(event);
2782
3309
  } catch {
2783
3310
  return false;
2784
3311
  }
@@ -3647,11 +4174,18 @@ function createCvmService(options) {
3647
4174
  sendByWindow.delete(windowId);
3648
4175
  for (const [pubkey, windows] of windowsByServer) {
3649
4176
  windows.delete(windowId);
3650
- if (windows.size === 0) windowsByServer.delete(pubkey);
4177
+ if (windows.size === 0) {
4178
+ windowsByServer.delete(pubkey);
4179
+ void transport.close({ pubkey }).catch(() => {
4180
+ });
4181
+ }
3651
4182
  }
3652
4183
  },
3653
4184
  dispose() {
3654
4185
  eventSub.close();
4186
+ transport.dispose?.();
4187
+ sendByWindow.clear();
4188
+ windowsByServer.clear();
3655
4189
  }
3656
4190
  };
3657
4191
  }
@@ -3840,35 +4374,102 @@ function unsupported(resultType, id) {
3840
4374
  error: `${resultType.replace(".result", "")} unavailable`
3841
4375
  };
3842
4376
  }
4377
+ function createContext(windowId, emit) {
4378
+ return {
4379
+ windowId,
4380
+ emit
4381
+ };
4382
+ }
3843
4383
  function createSerialService(options = {}) {
4384
+ const windows = /* @__PURE__ */ new Map();
4385
+ const currentWindow = (windowId) => {
4386
+ const existing = windows.get(windowId);
4387
+ if (existing) return existing;
4388
+ const record = {
4389
+ connections: /* @__PURE__ */ new Map(),
4390
+ pendingConnections: /* @__PURE__ */ new Set()
4391
+ };
4392
+ windows.set(windowId, record);
4393
+ return record;
4394
+ };
4395
+ const emit = (windowId, record, connection, send, event) => {
4396
+ if (!connection || windows.get(windowId) !== record || connection.state === "closed") return;
4397
+ if (connection.state === "opening") {
4398
+ connection.bufferedEvents.push(event);
4399
+ return;
4400
+ }
4401
+ send({ type: "serial.event", event });
4402
+ };
3844
4403
  return {
3845
4404
  descriptor: SERIAL_DESCRIPTOR,
3846
4405
  handleMessage(windowId, message, send) {
3847
4406
  const id = message.id ?? "";
3848
- const context = { windowId };
4407
+ const record = currentWindow(windowId);
4408
+ const isCurrent = () => windows.get(windowId) === record;
4409
+ const currentSend = (outbound) => {
4410
+ if (isCurrent()) send(outbound);
4411
+ };
3849
4412
  if (message.type === "serial.open") {
3850
4413
  if (!options.open) {
3851
- send(unsupported("serial.open.result", id));
4414
+ currentSend(unsupported("serial.open.result", id));
3852
4415
  return;
3853
4416
  }
3854
4417
  const serialMessage = message;
3855
- settle2(
3856
- () => options.open(serialMessage.request, context),
3857
- send,
3858
- (error) => ({ type: "serial.open.result", id, error }),
3859
- (result2) => ({ type: "serial.open.result", id, session: result2.session })
3860
- );
4418
+ const connection = { state: "opening", bufferedEvents: [] };
4419
+ record.pendingConnections.add(connection);
4420
+ const context = createContext(windowId, (event) => emit(windowId, record, connection, currentSend, event));
4421
+ let pending;
4422
+ try {
4423
+ pending = Promise.resolve(options.open(serialMessage.request, context));
4424
+ } catch (error) {
4425
+ record.pendingConnections.delete(connection);
4426
+ connection.state = "closed";
4427
+ currentSend({
4428
+ type: "serial.open.result",
4429
+ id,
4430
+ error: errorMessage2(error, "serial request failed")
4431
+ });
4432
+ return;
4433
+ }
4434
+ void pending.then((result2) => {
4435
+ record.pendingConnections.delete(connection);
4436
+ if (!isCurrent() || connection.state === "closed") return;
4437
+ connection.state = "open";
4438
+ record.connections.set(result2.session.id, connection);
4439
+ currentSend({ type: "serial.open.result", id, session: result2.session });
4440
+ if (!isCurrent() || connection.state !== "open") return;
4441
+ for (const event of connection.bufferedEvents) {
4442
+ if (!isCurrent() || connection.state !== "open") break;
4443
+ currentSend({ type: "serial.event", event });
4444
+ }
4445
+ connection.bufferedEvents.length = 0;
4446
+ }).catch((error) => {
4447
+ record.pendingConnections.delete(connection);
4448
+ connection.state = "closed";
4449
+ currentSend({
4450
+ type: "serial.open.result",
4451
+ id,
4452
+ error: errorMessage2(error, "serial request failed")
4453
+ });
4454
+ });
3861
4455
  return;
3862
4456
  }
3863
4457
  if (message.type === "serial.write") {
3864
4458
  if (!options.write) {
3865
- send(unsupported("serial.write.result", id));
4459
+ currentSend(unsupported("serial.write.result", id));
3866
4460
  return;
3867
4461
  }
3868
4462
  const serialMessage = message;
4463
+ const context = createContext(windowId, (event) => emit(
4464
+ windowId,
4465
+ record,
4466
+ record.connections.get(serialMessage.sessionId),
4467
+ currentSend,
4468
+ event
4469
+ ));
3869
4470
  settle2(
3870
4471
  () => options.write(serialMessage.sessionId, serialMessage.data, context),
3871
- send,
4472
+ currentSend,
3872
4473
  (error) => ({ type: "serial.write.result", id, error }),
3873
4474
  () => ({ type: "serial.write.result", id })
3874
4475
  );
@@ -3876,25 +4477,45 @@ function createSerialService(options = {}) {
3876
4477
  }
3877
4478
  if (message.type === "serial.close") {
3878
4479
  if (!options.close) {
3879
- send(unsupported("serial.close.result", id));
4480
+ currentSend(unsupported("serial.close.result", id));
3880
4481
  return;
3881
4482
  }
3882
4483
  const serialMessage = message;
4484
+ const connection = record.connections.get(serialMessage.sessionId);
4485
+ if (connection) {
4486
+ connection.state = "closed";
4487
+ record.connections.delete(serialMessage.sessionId);
4488
+ }
4489
+ const context = createContext(windowId, (event) => emit(
4490
+ windowId,
4491
+ record,
4492
+ connection,
4493
+ currentSend,
4494
+ event
4495
+ ));
3883
4496
  settle2(
3884
4497
  () => options.close(serialMessage.sessionId, serialMessage.reason, context),
3885
- send,
4498
+ currentSend,
3886
4499
  (error) => ({ type: "serial.close.result", id, error }),
3887
4500
  () => ({ type: "serial.close.result", id })
3888
4501
  );
3889
4502
  return;
3890
4503
  }
3891
- send({
4504
+ currentSend({
3892
4505
  type: `${message.type}.error`,
3893
4506
  id,
3894
4507
  error: `Unknown serial method: ${message.type}`
3895
4508
  });
3896
4509
  },
3897
4510
  onWindowDestroyed(windowId) {
4511
+ const record = windows.get(windowId);
4512
+ if (record) {
4513
+ for (const connection of record.connections.values()) connection.state = "closed";
4514
+ for (const connection of record.pendingConnections) connection.state = "closed";
4515
+ record.connections.clear();
4516
+ record.pendingConnections.clear();
4517
+ windows.delete(windowId);
4518
+ }
3898
4519
  options.destroyWindow?.(windowId);
3899
4520
  }
3900
4521
  };
@@ -3929,7 +4550,7 @@ function unsupported2(resultType, id) {
3929
4550
  error: `${resultType.replace(".result", "")} unavailable`
3930
4551
  };
3931
4552
  }
3932
- function createContext(windowId, send) {
4553
+ function createContext2(windowId, send) {
3933
4554
  return {
3934
4555
  windowId,
3935
4556
  emit(event) {
@@ -3942,7 +4563,7 @@ function createBleService(options = {}) {
3942
4563
  descriptor: BLE_DESCRIPTOR,
3943
4564
  handleMessage(windowId, message, send) {
3944
4565
  const id = message.id ?? "";
3945
- const context = createContext(windowId, send);
4566
+ const context = createContext2(windowId, send);
3946
4567
  if (message.type === "ble.open") {
3947
4568
  if (!options.open) {
3948
4569
  send(unsupported2("ble.open.result", id));
@@ -4082,7 +4703,7 @@ function unsupported3(resultType, id) {
4082
4703
  error: `${resultType.replace(".result", "")} unavailable`
4083
4704
  };
4084
4705
  }
4085
- function createContext2(windowId, send) {
4706
+ function createContext3(windowId, send) {
4086
4707
  return {
4087
4708
  windowId,
4088
4709
  emit(event) {
@@ -4095,7 +4716,7 @@ function createWebrtcService(options = {}) {
4095
4716
  descriptor: WEBRTC_DESCRIPTOR,
4096
4717
  handleMessage(windowId, message, send) {
4097
4718
  const id = message.id ?? "";
4098
- const context = createContext2(windowId, send);
4719
+ const context = createContext3(windowId, send);
4099
4720
  if (message.type === "webrtc.open") {
4100
4721
  if (!options.open) {
4101
4722
  send(unsupported3("webrtc.open.result", id));
@@ -4417,7 +5038,7 @@ function createDmService(options) {
4417
5038
  }
4418
5039
  subs.add(subscriptionId);
4419
5040
  }
4420
- function forget(windowId, subscriptionId) {
5041
+ function forget2(windowId, subscriptionId) {
4421
5042
  const subs = ownedSubscriptions.get(windowId);
4422
5043
  if (!subs) return;
4423
5044
  subs.delete(subscriptionId);
@@ -4430,33 +5051,40 @@ function createDmService(options) {
4430
5051
  const requestId = typeof id === "string" ? id : "";
4431
5052
  switch (message.type) {
4432
5053
  case "dm.status":
4433
- void Promise.resolve(adapter.status()).then((status) => send({ type: "dm.status.result", id: requestId, ...status })).catch((error) => send({ type: "dm.status.result", id: requestId, error: err(error) }));
5054
+ void Promise.resolve().then(() => adapter.status()).then((status) => send({ type: "dm.status.result", id: requestId, ...status })).catch((error) => send({ type: "dm.status.result", id: requestId, error: err(error) }));
4434
5055
  return;
4435
5056
  case "dm.conversations":
4436
- void Promise.resolve(adapter.conversations(message)).then((page) => send({ type: "dm.conversations.result", id: requestId, ...page })).catch(
4437
- (error) => send({ type: "dm.conversations.result", id: requestId, conversations: [], error: err(error) })
5057
+ void Promise.resolve().then(() => adapter.conversations(message)).then((page) => send({ type: "dm.conversations.result", id: requestId, ...page })).catch(
5058
+ (error) => send({ type: "dm.conversations.result", id: requestId, error: err(error) })
4438
5059
  );
4439
5060
  return;
4440
5061
  case "dm.messages":
4441
- void Promise.resolve(adapter.messages(message)).then((page) => send({ type: "dm.messages.result", id: requestId, ...page })).catch(
4442
- (error) => send({ type: "dm.messages.result", id: requestId, messages: [], error: err(error) })
5062
+ void Promise.resolve().then(() => adapter.messages(message)).then((page) => send({ type: "dm.messages.result", id: requestId, ...page })).catch(
5063
+ (error) => send({ type: "dm.messages.result", id: requestId, error: err(error) })
4443
5064
  );
4444
5065
  return;
4445
5066
  case "dm.send":
4446
- void adapter.send(message).then((result2) => send({ type: "dm.send.result", id: requestId, ...result2 })).catch((error) => send({ type: "dm.send.result", id: requestId, error: err(error) }));
5067
+ void Promise.resolve().then(() => adapter.send(message)).then((result2) => send({ type: "dm.send.result", id: requestId, ...result2 })).catch((error) => send({ type: "dm.send.result", id: requestId, error: err(error) }));
4447
5068
  return;
4448
5069
  case "dm.subscribe": {
4449
5070
  let activeSubscriptionId = "";
4450
- void Promise.resolve(
4451
- adapter.subscribe(message, (dmMessage) => {
5071
+ const pending = [];
5072
+ void Promise.resolve().then(
5073
+ () => adapter.subscribe(message, (dmMessage) => {
4452
5074
  if (activeSubscriptionId.length > 0) {
4453
5075
  send({ type: "dm.message", subscriptionId: activeSubscriptionId, message: dmMessage });
5076
+ } else {
5077
+ pending.push(dmMessage);
4454
5078
  }
4455
5079
  })
4456
5080
  ).then((subscription) => {
4457
5081
  remember(windowId, subscription.subscriptionId);
4458
5082
  activeSubscriptionId = subscription.subscriptionId;
4459
5083
  send({ type: "dm.subscribe.result", id: requestId, ...subscription });
5084
+ for (const dmMessage of pending) {
5085
+ send({ type: "dm.message", subscriptionId: activeSubscriptionId, message: dmMessage });
5086
+ }
5087
+ pending.length = 0;
4460
5088
  }).catch((error) => {
4461
5089
  send({ type: "dm.subscribe.result", id: requestId, error: err(error) });
4462
5090
  });
@@ -4464,14 +5092,14 @@ function createDmService(options) {
4464
5092
  }
4465
5093
  case "dm.unsubscribe": {
4466
5094
  const subId = message.subscriptionId;
4467
- if (typeof subId !== "string" || subId.length === 0) {
4468
- send({ type: "dm.unsubscribe.result", id: requestId, ok: false, error: "subscription not found" });
5095
+ if (typeof subId !== "string" || subId.length === 0 || ownedSubscriptions.get(windowId)?.has(subId) !== true) {
5096
+ send({ type: "dm.unsubscribe.result", id: requestId, error: "subscription not found" });
4469
5097
  return;
4470
5098
  }
4471
- void Promise.resolve(adapter.unsubscribe(subId)).then((ok) => {
4472
- forget(windowId, subId);
5099
+ void Promise.resolve().then(() => adapter.unsubscribe(subId)).then((ok) => {
5100
+ forget2(windowId, subId);
4473
5101
  send({ type: "dm.unsubscribe.result", id: requestId, ...ok });
4474
- }).catch((error) => send({ type: "dm.unsubscribe.result", id: requestId, ok: false, error: err(error) }));
5102
+ }).catch((error) => send({ type: "dm.unsubscribe.result", id: requestId, error: err(error) }));
4475
5103
  return;
4476
5104
  }
4477
5105
  default:
@@ -4492,7 +5120,13 @@ function createDmService(options) {
4492
5120
  }
4493
5121
 
4494
5122
  // src/dm-nip17-adapter.ts
4495
- import { getPublicKey } from "nostr-tools/pure";
5123
+ import * as nip44 from "nostr-tools/nip44";
5124
+ import {
5125
+ getEventHash,
5126
+ getPublicKey,
5127
+ validateEvent,
5128
+ verifyEvent
5129
+ } from "nostr-tools/pure";
4496
5130
  import * as nip17 from "nostr-tools/nip17";
4497
5131
 
4498
5132
  // src/dm-memory-store.ts
@@ -4542,6 +5176,7 @@ var DmMemoryStore = class {
4542
5176
 
4543
5177
  // src/dm-nip17-adapter.ts
4544
5178
  var NIP17_GIFT_WRAP_KIND = 1059;
5179
+ var NIP59_SEAL_KIND = 13;
4545
5180
  var subscriptionCounter = 0;
4546
5181
  function tagsFor(tags, name) {
4547
5182
  return tags.filter((tag) => tag[0] === name && typeof tag[1] === "string").map((tag) => tag[1]);
@@ -4549,110 +5184,163 @@ function tagsFor(tags, name) {
4549
5184
  function nowSeconds() {
4550
5185
  return Math.floor(Date.now() / 1e3);
4551
5186
  }
4552
- function createNip17DmAdapter(options) {
4553
- const ownerPubkey = getPublicKey(options.ownerSecretKey);
4554
- const store = options.store ?? new DmMemoryStore();
4555
- const live = /* @__PURE__ */ new Map();
4556
- function relaysFor2(filters) {
4557
- return options.relays && options.relays.length > 0 ? options.relays : options.relayPool.selectRelayTier(filters);
5187
+ var Nip17DmRuntime = class {
5188
+ constructor(options) {
5189
+ this.options = options;
5190
+ this.ownerPubkey = getPublicKey(options.ownerSecretKey);
5191
+ this.store = options.store ?? new DmMemoryStore();
5192
+ }
5193
+ options;
5194
+ ownerPubkey;
5195
+ store;
5196
+ live = /* @__PURE__ */ new Map();
5197
+ historyLoaded = false;
5198
+ historyLoading = null;
5199
+ adapter() {
5200
+ return {
5201
+ status: () => this.status(),
5202
+ conversations: (query) => this.conversations(query),
5203
+ messages: (query) => this.messages(query),
5204
+ send: (request) => this.send(request),
5205
+ subscribe: (request, onMessage) => this.subscribe(request, onMessage),
5206
+ unsubscribe: (subscriptionId) => this.unsubscribe(subscriptionId),
5207
+ close: () => this.close()
5208
+ };
4558
5209
  }
4559
- function normalizeRumor(rumor, status) {
5210
+ relaysFor(filters) {
5211
+ return this.options.relays && this.options.relays.length > 0 ? this.options.relays : this.options.relayPool.selectRelayTier(filters);
5212
+ }
5213
+ normalizeRumor(rumor, status) {
4560
5214
  if (rumor.kind !== 14) return null;
4561
- const participants = [.../* @__PURE__ */ new Set([ownerPubkey, rumor.pubkey, ...tagsFor(rumor.tags, "p")])];
4562
- const conversationId = store.conversationIdFor(participants);
4563
- return store.upsertMessage(
4564
- {
4565
- id: rumor.id,
4566
- conversationId,
4567
- senderPubkey: rumor.pubkey,
4568
- createdAt: rumor.created_at,
4569
- content: rumor.content,
4570
- status
4571
- },
4572
- participants
4573
- );
5215
+ const participants = [.../* @__PURE__ */ new Set([this.ownerPubkey, rumor.pubkey, ...tagsFor(rumor.tags, "p")])];
5216
+ const conversationId = this.store.conversationIdFor(participants);
5217
+ return this.store.upsertMessage({
5218
+ id: rumor.id,
5219
+ conversationId,
5220
+ senderPubkey: rumor.pubkey,
5221
+ createdAt: rumor.created_at,
5222
+ content: rumor.content,
5223
+ status
5224
+ }, participants);
5225
+ }
5226
+ decryptEvent(event) {
5227
+ return JSON.parse(nip44.decrypt(
5228
+ event.content,
5229
+ nip44.getConversationKey(this.options.ownerSecretKey, event.pubkey)
5230
+ ));
5231
+ }
5232
+ unwrapVerified(wrap) {
5233
+ if (wrap.kind !== NIP17_GIFT_WRAP_KIND || !verifyEvent(wrap) || !wrap.tags.some((tag) => tag[0] === "p" && tag[1] === this.ownerPubkey)) return null;
5234
+ try {
5235
+ const seal = this.decryptEvent(wrap);
5236
+ if (!validateEvent(seal) || typeof seal.id !== "string" || typeof seal.sig !== "string" || seal.kind !== NIP59_SEAL_KIND) return null;
5237
+ const signedSeal = seal;
5238
+ if (!verifyEvent(signedSeal)) return null;
5239
+ const rumor = this.decryptEvent(signedSeal);
5240
+ if (!validateEvent(rumor)) return null;
5241
+ const candidate = rumor;
5242
+ if (candidate.kind !== 14 || candidate.pubkey !== signedSeal.pubkey || typeof candidate.id !== "string" || candidate.id !== getEventHash(candidate)) return null;
5243
+ return candidate;
5244
+ } catch {
5245
+ return null;
5246
+ }
4574
5247
  }
4575
- return {
4576
- status() {
4577
- return {
4578
- available: options.relayPool.isAvailable(),
4579
- ownerPubkey,
4580
- implementations: ["nip17"],
4581
- capabilities: ["send", "receive", "subscribe", "history"]
4582
- };
4583
- },
4584
- conversations(query) {
4585
- return store.conversations(query);
4586
- },
4587
- messages(query) {
4588
- return store.messages(query);
4589
- },
4590
- async send(request) {
4591
- if (!options.relayPool.isAvailable()) throw new Error("relay unavailable");
4592
- if (!Array.isArray(request.recipients) || request.recipients.length === 0) {
4593
- throw new Error("recipient required");
4594
- }
4595
- if (typeof request.content !== "string" || request.content.length === 0) {
4596
- throw new Error("content required");
5248
+ ingestWrap(wrap, status) {
5249
+ const rumor = this.unwrapVerified(wrap);
5250
+ return rumor ? this.normalizeRumor(rumor, status) : null;
5251
+ }
5252
+ async loadHistory() {
5253
+ if (this.historyLoaded || !this.options.relayPool.query) return;
5254
+ this.historyLoading ??= (async () => {
5255
+ const filters = [{ kinds: [NIP17_GIFT_WRAP_KIND], "#p": [this.ownerPubkey], limit: 1e3 }];
5256
+ const wraps = await this.options.relayPool.query(filters, this.relaysFor(filters));
5257
+ for (const wrap of wraps) this.ingestWrap(wrap, "received");
5258
+ this.historyLoaded = true;
5259
+ })().finally(() => {
5260
+ this.historyLoading = null;
5261
+ });
5262
+ await this.historyLoading;
5263
+ }
5264
+ status() {
5265
+ return {
5266
+ available: this.options.relayPool.isAvailable(),
5267
+ ownerPubkey: this.ownerPubkey,
5268
+ implementations: ["nip17"],
5269
+ capabilities: ["send", "receive", "subscribe", "history"]
5270
+ };
5271
+ }
5272
+ async conversations(query) {
5273
+ await this.loadHistory();
5274
+ return this.store.conversations(query);
5275
+ }
5276
+ async messages(query) {
5277
+ await this.loadHistory();
5278
+ return this.store.messages(query);
5279
+ }
5280
+ async send(request) {
5281
+ if (!this.options.relayPool.isAvailable()) throw new Error("relay unavailable");
5282
+ if (!Array.isArray(request.recipients) || request.recipients.length === 0) throw new Error("invalid recipient");
5283
+ if (request.recipients.some((recipient) => !/^[0-9a-f]{64}$/.test(recipient))) throw new Error("invalid recipient");
5284
+ if (typeof request.content !== "string" || request.content.length === 0) throw new Error("content required");
5285
+ if (this.options.authorizeSend && !await this.options.authorizeSend(request)) throw new Error("forbidden");
5286
+ const recipients = [...new Set(request.recipients)];
5287
+ const wraps = nip17.wrapManyEvents(
5288
+ this.options.ownerSecretKey,
5289
+ recipients.map((publicKey) => ({ publicKey })),
5290
+ request.content
5291
+ );
5292
+ for (const wrap of wraps) await Promise.resolve(this.options.relayPool.publish(wrap));
5293
+ const conversationId = request.conversationId ?? this.store.conversationIdFor([this.ownerPubkey, ...recipients]);
5294
+ const message = this.store.upsertMessage({
5295
+ id: request.clientMessageId ?? wraps[0]?.id ?? `dm-${nowSeconds()}`,
5296
+ conversationId,
5297
+ senderPubkey: this.ownerPubkey,
5298
+ createdAt: nowSeconds(),
5299
+ content: request.content,
5300
+ status: "sent"
5301
+ }, [this.ownerPubkey, ...recipients]);
5302
+ return { ok: true, message };
5303
+ }
5304
+ async subscribe(request, onMessage) {
5305
+ if (!this.options.relayPool.isAvailable()) throw new Error("relay unavailable");
5306
+ await this.loadHistory();
5307
+ const subscriptionId = `dm-nip17-${++subscriptionCounter}`;
5308
+ const filters = [{ kinds: [NIP17_GIFT_WRAP_KIND], "#p": [this.ownerPubkey] }];
5309
+ const sub = { conversationId: request.conversationId, onMessage };
5310
+ this.live.set(subscriptionId, sub);
5311
+ try {
5312
+ const handle = this.options.relayPool.subscribe(filters, (item) => {
5313
+ if (item === "EOSE") return;
5314
+ const message = this.ingestWrap(item, "received");
5315
+ if (!message) return;
5316
+ const active = this.live.get(subscriptionId);
5317
+ if (active && (!active.conversationId || active.conversationId === message.conversationId)) active.onMessage(message);
5318
+ }, this.relaysFor(filters));
5319
+ if (this.live.get(subscriptionId) !== sub) {
5320
+ handle.unsubscribe();
5321
+ throw new Error("subscription cancelled");
4597
5322
  }
4598
- const recipients = [...new Set(request.recipients)];
4599
- const wraps = nip17.wrapManyEvents(
4600
- options.ownerSecretKey,
4601
- recipients.map((publicKey) => ({ publicKey })),
4602
- request.content
4603
- );
4604
- for (const wrap of wraps) await Promise.resolve(options.relayPool.publish(wrap));
4605
- const conversationId = request.conversationId ?? store.conversationIdFor([ownerPubkey, ...recipients]);
4606
- const message = store.upsertMessage(
4607
- {
4608
- id: request.clientMessageId ?? wraps[0]?.id ?? `dm-${nowSeconds()}`,
4609
- conversationId,
4610
- senderPubkey: ownerPubkey,
4611
- createdAt: nowSeconds(),
4612
- content: request.content,
4613
- status: "sent"
4614
- },
4615
- [ownerPubkey, ...recipients]
4616
- );
4617
- return { ok: true, message };
4618
- },
4619
- subscribe(request, onMessage) {
4620
- if (!options.relayPool.isAvailable()) throw new Error("relay unavailable");
4621
- const subscriptionId = `dm-nip17-${++subscriptionCounter}`;
4622
- const filters = [{ kinds: [NIP17_GIFT_WRAP_KIND], "#p": [ownerPubkey] }];
4623
- const handle = options.relayPool.subscribe(
4624
- filters,
4625
- (item) => {
4626
- if (item === "EOSE") return;
4627
- let rumor;
4628
- try {
4629
- rumor = nip17.unwrapEvent(item, options.ownerSecretKey);
4630
- } catch {
4631
- return;
4632
- }
4633
- const message = normalizeRumor(rumor, "received");
4634
- if (!message) return;
4635
- for (const sub of live.values()) {
4636
- if (!sub.conversationId || sub.conversationId === message.conversationId) sub.onMessage(message);
4637
- }
4638
- },
4639
- relaysFor2(filters)
4640
- );
4641
- live.set(subscriptionId, { handle, conversationId: request.conversationId, onMessage });
5323
+ sub.handle = handle;
4642
5324
  return { subscriptionId };
4643
- },
4644
- unsubscribe(subscriptionId) {
4645
- const sub = live.get(subscriptionId);
4646
- if (!sub) return { ok: false };
4647
- sub.handle.unsubscribe();
4648
- live.delete(subscriptionId);
4649
- return { ok: true };
4650
- },
4651
- close() {
4652
- for (const sub of live.values()) sub.handle.unsubscribe();
4653
- live.clear();
5325
+ } catch (error) {
5326
+ this.live.delete(subscriptionId);
5327
+ throw error;
4654
5328
  }
4655
- };
5329
+ }
5330
+ unsubscribe(subscriptionId) {
5331
+ const sub = this.live.get(subscriptionId);
5332
+ if (!sub) return { ok: false };
5333
+ sub.handle?.unsubscribe();
5334
+ this.live.delete(subscriptionId);
5335
+ return { ok: true };
5336
+ }
5337
+ close() {
5338
+ for (const sub of this.live.values()) sub.handle?.unsubscribe();
5339
+ this.live.clear();
5340
+ }
5341
+ };
5342
+ function createNip17DmAdapter(options) {
5343
+ return new Nip17DmRuntime(options).adapter();
4656
5344
  }
4657
5345
 
4658
5346
  // src/dm-ndr-adapter.ts
@@ -5033,8 +5721,154 @@ function createCordnDmAdapter(options) {
5033
5721
  }
5034
5722
  };
5035
5723
  }
5724
+
5725
+ // src/fs-service.ts
5726
+ var FsServiceError = class extends Error {
5727
+ constructor(code, message = code) {
5728
+ super(message);
5729
+ this.code = code;
5730
+ this.name = "FsServiceError";
5731
+ }
5732
+ code;
5733
+ };
5734
+ var FS_DESCRIPTOR = {
5735
+ name: "fs",
5736
+ version: "1.0.0",
5737
+ description: "NAP-FS runtime-owned virtual filesystem"
5738
+ };
5739
+ var watchCounter = 0;
5740
+ function fsError(error) {
5741
+ return error instanceof FsServiceError ? error.code : "io-error";
5742
+ }
5743
+ function createFsService(options) {
5744
+ if (!options?.backend) throw new Error("createFsService: options.backend is required");
5745
+ const watches = /* @__PURE__ */ new Map();
5746
+ const watchesByWindow = /* @__PURE__ */ new Map();
5747
+ function ownWatch(watchId, owned) {
5748
+ watches.set(watchId, owned);
5749
+ let ids = watchesByWindow.get(owned.windowId);
5750
+ if (!ids) {
5751
+ ids = /* @__PURE__ */ new Set();
5752
+ watchesByWindow.set(owned.windowId, ids);
5753
+ }
5754
+ ids.add(watchId);
5755
+ }
5756
+ function forgetWatch(watchId) {
5757
+ const owned = watches.get(watchId);
5758
+ if (!owned) return void 0;
5759
+ watches.delete(watchId);
5760
+ const ids = watchesByWindow.get(owned.windowId);
5761
+ ids?.delete(watchId);
5762
+ if (ids?.size === 0) watchesByWindow.delete(owned.windowId);
5763
+ return owned;
5764
+ }
5765
+ function result2(send, type, id, operation, field) {
5766
+ void Promise.resolve().then(operation).then((value) => send({
5767
+ type,
5768
+ id,
5769
+ ...field ? { [field]: value } : {}
5770
+ })).catch((error) => send({ type, id, error: fsError(error) }));
5771
+ }
5772
+ function closeWindowWatches(windowId) {
5773
+ for (const watchId of watchesByWindow.get(windowId) ?? []) {
5774
+ const owned = forgetWatch(watchId);
5775
+ if (owned?.handle) void Promise.resolve(owned.handle.close()).catch(() => void 0);
5776
+ }
5777
+ }
5778
+ return {
5779
+ descriptor: FS_DESCRIPTOR,
5780
+ handleMessage(windowId, message, send) {
5781
+ const input = message;
5782
+ const id = typeof input.id === "string" ? input.id : "";
5783
+ switch (input.type) {
5784
+ case "fs.info":
5785
+ result2(send, "fs.info.result", id, () => options.backend.info(windowId), "info");
5786
+ return;
5787
+ case "fs.pickFile":
5788
+ result2(send, "fs.pickFile.result", id, () => options.backend.pickFile(windowId, input.options), "result");
5789
+ return;
5790
+ case "fs.pickFiles":
5791
+ result2(send, "fs.pickFiles.result", id, () => options.backend.pickFiles(windowId, input.options), "result");
5792
+ return;
5793
+ case "fs.pickDirectory":
5794
+ result2(send, "fs.pickDirectory.result", id, () => options.backend.pickDirectory(windowId, input.options), "result");
5795
+ return;
5796
+ case "fs.pickSaveFile":
5797
+ result2(send, "fs.pickSaveFile.result", id, () => options.backend.pickSaveFile(windowId, input.options), "result");
5798
+ return;
5799
+ case "fs.stat":
5800
+ result2(send, "fs.stat.result", id, () => options.backend.stat(windowId, input.path), "metadata");
5801
+ return;
5802
+ case "fs.list":
5803
+ result2(send, "fs.list.result", id, () => options.backend.list(windowId, input.path), "entries");
5804
+ return;
5805
+ case "fs.read":
5806
+ result2(send, "fs.read.result", id, () => options.backend.read(windowId, input.path, input.options), "result");
5807
+ return;
5808
+ case "fs.write":
5809
+ result2(send, "fs.write.result", id, () => options.backend.write(windowId, input.path, input.data, input.options), "result");
5810
+ return;
5811
+ case "fs.mkdir":
5812
+ result2(send, "fs.mkdir.result", id, () => options.backend.mkdir(windowId, input.path, input.options));
5813
+ return;
5814
+ case "fs.remove":
5815
+ result2(send, "fs.remove.result", id, () => options.backend.remove(windowId, input.path, input.recursive));
5816
+ return;
5817
+ case "fs.move":
5818
+ result2(send, "fs.move.result", id, () => options.backend.move(windowId, input.fromPath, input.toPath));
5819
+ return;
5820
+ case "fs.watch": {
5821
+ const watchId = `fs-watch-${++watchCounter}`;
5822
+ result2(send, "fs.watch.result", id, async () => {
5823
+ const owned = { windowId };
5824
+ ownWatch(watchId, owned);
5825
+ const handle = await options.backend.watch(
5826
+ windowId,
5827
+ input.path,
5828
+ input.options,
5829
+ (change) => {
5830
+ if (watches.get(watchId) !== owned || owned.handle === void 0) return;
5831
+ send({ type: "fs.changed", change: { ...change, watchId } });
5832
+ }
5833
+ );
5834
+ if (watches.get(watchId) !== owned) {
5835
+ await Promise.resolve(handle.close()).catch(() => void 0);
5836
+ throw new FsServiceError("cancelled");
5837
+ }
5838
+ owned.handle = handle;
5839
+ return watchId;
5840
+ }, "watchId");
5841
+ return;
5842
+ }
5843
+ case "fs.unwatch": {
5844
+ const watchId = typeof input.watchId === "string" ? input.watchId : "";
5845
+ const owned = watches.get(watchId);
5846
+ if (!owned || owned.windowId !== windowId) {
5847
+ send({ type: "fs.unwatch.result", id });
5848
+ return;
5849
+ }
5850
+ const forgotten = forgetWatch(watchId);
5851
+ result2(send, "fs.unwatch.result", id, () => forgotten?.handle?.close());
5852
+ return;
5853
+ }
5854
+ default:
5855
+ return;
5856
+ }
5857
+ },
5858
+ onWindowDestroyed(windowId) {
5859
+ closeWindowWatches(windowId);
5860
+ options.backend.onWindowDestroyed?.(windowId);
5861
+ },
5862
+ dispose() {
5863
+ for (const windowId of watchesByWindow.keys()) closeWindowWatches(windowId);
5864
+ options.backend.close?.();
5865
+ }
5866
+ };
5867
+ }
5036
5868
  export {
5037
5869
  DmMemoryStore,
5870
+ FsServiceError,
5871
+ ResourceServiceError,
5038
5872
  createBleService,
5039
5873
  createBrowserMediaBridge,
5040
5874
  createCacheService,
@@ -5047,6 +5881,7 @@ export {
5047
5881
  createCountService,
5048
5882
  createCvmService,
5049
5883
  createDmService,
5884
+ createFsService,
5050
5885
  createHttpUploader,
5051
5886
  createIdentityService,
5052
5887
  createIntentService,
@@ -5067,6 +5902,8 @@ export {
5067
5902
  createThemeService,
5068
5903
  createUploadService,
5069
5904
  createWebrtcService,
5070
- manifestToIntentCatalogEntry
5905
+ manifestToIntentCatalogEntry,
5906
+ resolveConfigValues,
5907
+ validateConfigSchema
5071
5908
  };
5072
5909
  //# sourceMappingURL=index.js.map