@kehto/services 0.19.0 → 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;
1745
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);
1746
2123
  }
1747
- return { handler, publishValues };
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;
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");
2226
+ }
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;
@@ -2775,7 +3294,7 @@ function createRelayPoolOutboxRouter(options) {
2775
3294
  if (!Array.isArray(options.fallbackRelays)) {
2776
3295
  throw new Error("createRelayPoolOutboxRouter: options.fallbackRelays is required");
2777
3296
  }
2778
- const verifyEvent = options.verifyEvent;
3297
+ const verifyEvent2 = options.verifyEvent;
2779
3298
  const ctx = {
2780
3299
  relayPool: options.relayPool,
2781
3300
  loadRelayLists: options.loadRelayLists,
@@ -2784,9 +3303,9 @@ function createRelayPoolOutboxRouter(options) {
2784
3303
  isRelayAllowed: options.isRelayAllowed ?? defaultRelayAllowed,
2785
3304
  defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
2786
3305
  async verify(event) {
2787
- if (!verifyEvent) return true;
3306
+ if (!verifyEvent2) return true;
2788
3307
  try {
2789
- return await verifyEvent(event);
3308
+ return await verifyEvent2(event);
2790
3309
  } catch {
2791
3310
  return false;
2792
3311
  }
@@ -3655,11 +4174,18 @@ function createCvmService(options) {
3655
4174
  sendByWindow.delete(windowId);
3656
4175
  for (const [pubkey, windows] of windowsByServer) {
3657
4176
  windows.delete(windowId);
3658
- if (windows.size === 0) windowsByServer.delete(pubkey);
4177
+ if (windows.size === 0) {
4178
+ windowsByServer.delete(pubkey);
4179
+ void transport.close({ pubkey }).catch(() => {
4180
+ });
4181
+ }
3659
4182
  }
3660
4183
  },
3661
4184
  dispose() {
3662
4185
  eventSub.close();
4186
+ transport.dispose?.();
4187
+ sendByWindow.clear();
4188
+ windowsByServer.clear();
3663
4189
  }
3664
4190
  };
3665
4191
  }
@@ -3848,35 +4374,102 @@ function unsupported(resultType, id) {
3848
4374
  error: `${resultType.replace(".result", "")} unavailable`
3849
4375
  };
3850
4376
  }
4377
+ function createContext(windowId, emit) {
4378
+ return {
4379
+ windowId,
4380
+ emit
4381
+ };
4382
+ }
3851
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
+ };
3852
4403
  return {
3853
4404
  descriptor: SERIAL_DESCRIPTOR,
3854
4405
  handleMessage(windowId, message, send) {
3855
4406
  const id = message.id ?? "";
3856
- 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
+ };
3857
4412
  if (message.type === "serial.open") {
3858
4413
  if (!options.open) {
3859
- send(unsupported("serial.open.result", id));
4414
+ currentSend(unsupported("serial.open.result", id));
3860
4415
  return;
3861
4416
  }
3862
4417
  const serialMessage = message;
3863
- settle2(
3864
- () => options.open(serialMessage.request, context),
3865
- send,
3866
- (error) => ({ type: "serial.open.result", id, error }),
3867
- (result2) => ({ type: "serial.open.result", id, session: result2.session })
3868
- );
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
+ });
3869
4455
  return;
3870
4456
  }
3871
4457
  if (message.type === "serial.write") {
3872
4458
  if (!options.write) {
3873
- send(unsupported("serial.write.result", id));
4459
+ currentSend(unsupported("serial.write.result", id));
3874
4460
  return;
3875
4461
  }
3876
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
+ ));
3877
4470
  settle2(
3878
4471
  () => options.write(serialMessage.sessionId, serialMessage.data, context),
3879
- send,
4472
+ currentSend,
3880
4473
  (error) => ({ type: "serial.write.result", id, error }),
3881
4474
  () => ({ type: "serial.write.result", id })
3882
4475
  );
@@ -3884,25 +4477,45 @@ function createSerialService(options = {}) {
3884
4477
  }
3885
4478
  if (message.type === "serial.close") {
3886
4479
  if (!options.close) {
3887
- send(unsupported("serial.close.result", id));
4480
+ currentSend(unsupported("serial.close.result", id));
3888
4481
  return;
3889
4482
  }
3890
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
+ ));
3891
4496
  settle2(
3892
4497
  () => options.close(serialMessage.sessionId, serialMessage.reason, context),
3893
- send,
4498
+ currentSend,
3894
4499
  (error) => ({ type: "serial.close.result", id, error }),
3895
4500
  () => ({ type: "serial.close.result", id })
3896
4501
  );
3897
4502
  return;
3898
4503
  }
3899
- send({
4504
+ currentSend({
3900
4505
  type: `${message.type}.error`,
3901
4506
  id,
3902
4507
  error: `Unknown serial method: ${message.type}`
3903
4508
  });
3904
4509
  },
3905
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
+ }
3906
4519
  options.destroyWindow?.(windowId);
3907
4520
  }
3908
4521
  };
@@ -3937,7 +4550,7 @@ function unsupported2(resultType, id) {
3937
4550
  error: `${resultType.replace(".result", "")} unavailable`
3938
4551
  };
3939
4552
  }
3940
- function createContext(windowId, send) {
4553
+ function createContext2(windowId, send) {
3941
4554
  return {
3942
4555
  windowId,
3943
4556
  emit(event) {
@@ -3950,7 +4563,7 @@ function createBleService(options = {}) {
3950
4563
  descriptor: BLE_DESCRIPTOR,
3951
4564
  handleMessage(windowId, message, send) {
3952
4565
  const id = message.id ?? "";
3953
- const context = createContext(windowId, send);
4566
+ const context = createContext2(windowId, send);
3954
4567
  if (message.type === "ble.open") {
3955
4568
  if (!options.open) {
3956
4569
  send(unsupported2("ble.open.result", id));
@@ -4090,7 +4703,7 @@ function unsupported3(resultType, id) {
4090
4703
  error: `${resultType.replace(".result", "")} unavailable`
4091
4704
  };
4092
4705
  }
4093
- function createContext2(windowId, send) {
4706
+ function createContext3(windowId, send) {
4094
4707
  return {
4095
4708
  windowId,
4096
4709
  emit(event) {
@@ -4103,7 +4716,7 @@ function createWebrtcService(options = {}) {
4103
4716
  descriptor: WEBRTC_DESCRIPTOR,
4104
4717
  handleMessage(windowId, message, send) {
4105
4718
  const id = message.id ?? "";
4106
- const context = createContext2(windowId, send);
4719
+ const context = createContext3(windowId, send);
4107
4720
  if (message.type === "webrtc.open") {
4108
4721
  if (!options.open) {
4109
4722
  send(unsupported3("webrtc.open.result", id));
@@ -4425,7 +5038,7 @@ function createDmService(options) {
4425
5038
  }
4426
5039
  subs.add(subscriptionId);
4427
5040
  }
4428
- function forget(windowId, subscriptionId) {
5041
+ function forget2(windowId, subscriptionId) {
4429
5042
  const subs = ownedSubscriptions.get(windowId);
4430
5043
  if (!subs) return;
4431
5044
  subs.delete(subscriptionId);
@@ -4438,33 +5051,40 @@ function createDmService(options) {
4438
5051
  const requestId = typeof id === "string" ? id : "";
4439
5052
  switch (message.type) {
4440
5053
  case "dm.status":
4441
- 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) }));
4442
5055
  return;
4443
5056
  case "dm.conversations":
4444
- void Promise.resolve(adapter.conversations(message)).then((page) => send({ type: "dm.conversations.result", id: requestId, ...page })).catch(
4445
- (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) })
4446
5059
  );
4447
5060
  return;
4448
5061
  case "dm.messages":
4449
- void Promise.resolve(adapter.messages(message)).then((page) => send({ type: "dm.messages.result", id: requestId, ...page })).catch(
4450
- (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) })
4451
5064
  );
4452
5065
  return;
4453
5066
  case "dm.send":
4454
- 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) }));
4455
5068
  return;
4456
5069
  case "dm.subscribe": {
4457
5070
  let activeSubscriptionId = "";
4458
- void Promise.resolve(
4459
- adapter.subscribe(message, (dmMessage) => {
5071
+ const pending = [];
5072
+ void Promise.resolve().then(
5073
+ () => adapter.subscribe(message, (dmMessage) => {
4460
5074
  if (activeSubscriptionId.length > 0) {
4461
5075
  send({ type: "dm.message", subscriptionId: activeSubscriptionId, message: dmMessage });
5076
+ } else {
5077
+ pending.push(dmMessage);
4462
5078
  }
4463
5079
  })
4464
5080
  ).then((subscription) => {
4465
5081
  remember(windowId, subscription.subscriptionId);
4466
5082
  activeSubscriptionId = subscription.subscriptionId;
4467
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;
4468
5088
  }).catch((error) => {
4469
5089
  send({ type: "dm.subscribe.result", id: requestId, error: err(error) });
4470
5090
  });
@@ -4472,14 +5092,14 @@ function createDmService(options) {
4472
5092
  }
4473
5093
  case "dm.unsubscribe": {
4474
5094
  const subId = message.subscriptionId;
4475
- if (typeof subId !== "string" || subId.length === 0) {
4476
- 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" });
4477
5097
  return;
4478
5098
  }
4479
- void Promise.resolve(adapter.unsubscribe(subId)).then((ok) => {
4480
- forget(windowId, subId);
5099
+ void Promise.resolve().then(() => adapter.unsubscribe(subId)).then((ok) => {
5100
+ forget2(windowId, subId);
4481
5101
  send({ type: "dm.unsubscribe.result", id: requestId, ...ok });
4482
- }).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) }));
4483
5103
  return;
4484
5104
  }
4485
5105
  default:
@@ -4500,7 +5120,13 @@ function createDmService(options) {
4500
5120
  }
4501
5121
 
4502
5122
  // src/dm-nip17-adapter.ts
4503
- 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";
4504
5130
  import * as nip17 from "nostr-tools/nip17";
4505
5131
 
4506
5132
  // src/dm-memory-store.ts
@@ -4550,6 +5176,7 @@ var DmMemoryStore = class {
4550
5176
 
4551
5177
  // src/dm-nip17-adapter.ts
4552
5178
  var NIP17_GIFT_WRAP_KIND = 1059;
5179
+ var NIP59_SEAL_KIND = 13;
4553
5180
  var subscriptionCounter = 0;
4554
5181
  function tagsFor(tags, name) {
4555
5182
  return tags.filter((tag) => tag[0] === name && typeof tag[1] === "string").map((tag) => tag[1]);
@@ -4557,110 +5184,163 @@ function tagsFor(tags, name) {
4557
5184
  function nowSeconds() {
4558
5185
  return Math.floor(Date.now() / 1e3);
4559
5186
  }
4560
- function createNip17DmAdapter(options) {
4561
- const ownerPubkey = getPublicKey(options.ownerSecretKey);
4562
- const store = options.store ?? new DmMemoryStore();
4563
- const live = /* @__PURE__ */ new Map();
4564
- function relaysFor2(filters) {
4565
- 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
+ };
5209
+ }
5210
+ relaysFor(filters) {
5211
+ return this.options.relays && this.options.relays.length > 0 ? this.options.relays : this.options.relayPool.selectRelayTier(filters);
4566
5212
  }
4567
- function normalizeRumor(rumor, status) {
5213
+ normalizeRumor(rumor, status) {
4568
5214
  if (rumor.kind !== 14) return null;
4569
- const participants = [.../* @__PURE__ */ new Set([ownerPubkey, rumor.pubkey, ...tagsFor(rumor.tags, "p")])];
4570
- const conversationId = store.conversationIdFor(participants);
4571
- return store.upsertMessage(
4572
- {
4573
- id: rumor.id,
4574
- conversationId,
4575
- senderPubkey: rumor.pubkey,
4576
- createdAt: rumor.created_at,
4577
- content: rumor.content,
4578
- status
4579
- },
4580
- participants
4581
- );
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
+ }
4582
5247
  }
4583
- return {
4584
- status() {
4585
- return {
4586
- available: options.relayPool.isAvailable(),
4587
- ownerPubkey,
4588
- implementations: ["nip17"],
4589
- capabilities: ["send", "receive", "subscribe", "history"]
4590
- };
4591
- },
4592
- conversations(query) {
4593
- return store.conversations(query);
4594
- },
4595
- messages(query) {
4596
- return store.messages(query);
4597
- },
4598
- async send(request) {
4599
- if (!options.relayPool.isAvailable()) throw new Error("relay unavailable");
4600
- if (!Array.isArray(request.recipients) || request.recipients.length === 0) {
4601
- throw new Error("recipient required");
4602
- }
4603
- if (typeof request.content !== "string" || request.content.length === 0) {
4604
- 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");
4605
5322
  }
4606
- const recipients = [...new Set(request.recipients)];
4607
- const wraps = nip17.wrapManyEvents(
4608
- options.ownerSecretKey,
4609
- recipients.map((publicKey) => ({ publicKey })),
4610
- request.content
4611
- );
4612
- for (const wrap of wraps) await Promise.resolve(options.relayPool.publish(wrap));
4613
- const conversationId = request.conversationId ?? store.conversationIdFor([ownerPubkey, ...recipients]);
4614
- const message = store.upsertMessage(
4615
- {
4616
- id: request.clientMessageId ?? wraps[0]?.id ?? `dm-${nowSeconds()}`,
4617
- conversationId,
4618
- senderPubkey: ownerPubkey,
4619
- createdAt: nowSeconds(),
4620
- content: request.content,
4621
- status: "sent"
4622
- },
4623
- [ownerPubkey, ...recipients]
4624
- );
4625
- return { ok: true, message };
4626
- },
4627
- subscribe(request, onMessage) {
4628
- if (!options.relayPool.isAvailable()) throw new Error("relay unavailable");
4629
- const subscriptionId = `dm-nip17-${++subscriptionCounter}`;
4630
- const filters = [{ kinds: [NIP17_GIFT_WRAP_KIND], "#p": [ownerPubkey] }];
4631
- const handle = options.relayPool.subscribe(
4632
- filters,
4633
- (item) => {
4634
- if (item === "EOSE") return;
4635
- let rumor;
4636
- try {
4637
- rumor = nip17.unwrapEvent(item, options.ownerSecretKey);
4638
- } catch {
4639
- return;
4640
- }
4641
- const message = normalizeRumor(rumor, "received");
4642
- if (!message) return;
4643
- for (const sub of live.values()) {
4644
- if (!sub.conversationId || sub.conversationId === message.conversationId) sub.onMessage(message);
4645
- }
4646
- },
4647
- relaysFor2(filters)
4648
- );
4649
- live.set(subscriptionId, { handle, conversationId: request.conversationId, onMessage });
5323
+ sub.handle = handle;
4650
5324
  return { subscriptionId };
4651
- },
4652
- unsubscribe(subscriptionId) {
4653
- const sub = live.get(subscriptionId);
4654
- if (!sub) return { ok: false };
4655
- sub.handle.unsubscribe();
4656
- live.delete(subscriptionId);
4657
- return { ok: true };
4658
- },
4659
- close() {
4660
- for (const sub of live.values()) sub.handle.unsubscribe();
4661
- live.clear();
5325
+ } catch (error) {
5326
+ this.live.delete(subscriptionId);
5327
+ throw error;
4662
5328
  }
4663
- };
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();
4664
5344
  }
4665
5345
 
4666
5346
  // src/dm-ndr-adapter.ts
@@ -5041,8 +5721,154 @@ function createCordnDmAdapter(options) {
5041
5721
  }
5042
5722
  };
5043
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
+ }
5044
5868
  export {
5045
5869
  DmMemoryStore,
5870
+ FsServiceError,
5871
+ ResourceServiceError,
5046
5872
  createBleService,
5047
5873
  createBrowserMediaBridge,
5048
5874
  createCacheService,
@@ -5055,6 +5881,7 @@ export {
5055
5881
  createCountService,
5056
5882
  createCvmService,
5057
5883
  createDmService,
5884
+ createFsService,
5058
5885
  createHttpUploader,
5059
5886
  createIdentityService,
5060
5887
  createIntentService,
@@ -5075,6 +5902,8 @@ export {
5075
5902
  createThemeService,
5076
5903
  createUploadService,
5077
5904
  createWebrtcService,
5078
- manifestToIntentCatalogEntry
5905
+ manifestToIntentCatalogEntry,
5906
+ resolveConfigValues,
5907
+ validateConfigSchema
5079
5908
  };
5080
5909
  //# sourceMappingURL=index.js.map