@kehto/services 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,202 +1693,626 @@ 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;
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;
1745
2218
  }
1746
2219
  }
1747
- return { handler, publishValues };
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
- if (typeof options?.fetch !== "function" || typeof options?.isOriginGranted !== "function" || typeof options?.getConnectGrants !== "function" || typeof options?.resolveIdentity !== "function") {
2275
+ if (typeof options?.fetch !== "function") {
2276
+ throw new Error(
2277
+ "[RESOURCE-01] createResourceService requires a runtime-provided fetch resolver."
2278
+ );
2279
+ }
2280
+ const hasOriginPolicy = options.isOriginGranted !== void 0 || options.getConnectGrants !== void 0;
2281
+ if (hasOriginPolicy && (typeof options.isOriginGranted !== "function" || typeof options.getConnectGrants !== "function" || typeof options.resolveIdentity !== "function")) {
1766
2282
  throw new Error(
1767
- "[RESOURCE-01 / H-03] createResourceService requires {fetch, isOriginGranted, getConnectGrants, resolveIdentity} \u2014 all four options are required from day one. The grants source (getConnectGrants) MUST be wired at construction time to prevent unguarded fetch proxying."
2283
+ "[RESOURCE-01] optional origin policy requires {isOriginGranted, getConnectGrants, resolveIdentity} together."
1768
2284
  );
1769
2285
  }
2286
+ if (options.resolveIdentity !== void 0 && typeof options.resolveIdentity !== "function") {
2287
+ throw new Error("[RESOURCE-01] resolveIdentity must be a function when provided.");
2288
+ }
1770
2289
  }
1771
2290
  function trackRequest(state, requestId, windowId, controller) {
1772
- state.inFlight.set(requestId, { controller, windowId });
2291
+ const key = requestKey(windowId, requestId);
2292
+ state.inFlight.set(key, { controller, windowId, requestId });
1773
2293
  if (!state.perWindow.has(windowId)) {
1774
2294
  state.perWindow.set(windowId, /* @__PURE__ */ new Set());
1775
2295
  }
1776
- state.perWindow.get(windowId).add(requestId);
2296
+ state.perWindow.get(windowId).add(key);
1777
2297
  }
1778
- function untrackRequest(state, requestId) {
1779
- const entry = state.inFlight.get(requestId);
2298
+ function untrackRequest(state, windowId, requestId) {
2299
+ const key = requestKey(windowId, requestId);
2300
+ const entry = state.inFlight.get(key);
1780
2301
  if (entry) {
1781
- state.inFlight.delete(requestId);
1782
- state.perWindow.get(entry.windowId)?.delete(requestId);
2302
+ state.inFlight.delete(key);
2303
+ state.perWindow.get(entry.windowId)?.delete(key);
1783
2304
  }
1784
2305
  }
1785
- function sendResourceError(send, requestId, code, message, error = toResourceError(code), type = "resource.bytes.error") {
2306
+ function sendResourceError(send, requestId, message, error, type = "resource.bytes.error") {
1786
2307
  send({
1787
2308
  type,
1788
2309
  id: requestId,
1789
- requestId,
1790
2310
  error,
1791
- code,
1792
2311
  message
1793
2312
  });
1794
2313
  }
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
- }
2314
+ function sendBytesManyError(send, requestId, message, error) {
2315
+ sendResourceError(send, requestId, message, error, "resource.bytesMany.error");
1809
2316
  }
1810
2317
  function parseResourceUrl(url) {
1811
2318
  try {
@@ -1814,15 +2321,8 @@ function parseResourceUrl(url) {
1814
2321
  return null;
1815
2322
  }
1816
2323
  }
1817
- function collectResponseHeaders(response) {
1818
- const headers = {};
1819
- response.headers.forEach((value, key) => {
1820
- headers[key] = value;
1821
- });
1822
- return headers;
1823
- }
1824
2324
  function responseMime(response) {
1825
- return response.headers.get("content-type") || "application/octet-stream";
2325
+ return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || "application/octet-stream";
1826
2326
  }
1827
2327
  function responseBlob(buffer, mime) {
1828
2328
  return new Blob([buffer], { type: mime });
@@ -1841,7 +2341,7 @@ function normalizeResourceInfo(info) {
1841
2341
  }
1842
2342
  async function resolveResourceInfo(options, windowId) {
1843
2343
  const configured = options.resourceInfo ?? DEFAULT_RESOURCE_INFO;
1844
- const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity(windowId) }) : configured;
2344
+ const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity?.(windowId) ?? null }) : configured;
1845
2345
  return normalizeResourceInfo(info);
1846
2346
  }
1847
2347
  async function handleInfo(options, windowId, requestId, send) {
@@ -1866,91 +2366,108 @@ function resourceInvalidRequest(url, message) {
1866
2366
  ok: false,
1867
2367
  url,
1868
2368
  error: "invalid-request",
1869
- code: "invalid-url",
1870
2369
  message
1871
2370
  };
1872
2371
  }
1873
- async function fetchResourceItem(options, identity, url, init, signal) {
2372
+ function classifyResourceFailure(error) {
2373
+ return {
2374
+ error: error instanceof ResourceServiceError ? error.code : "network-error",
2375
+ message: error instanceof Error ? error.message : String(error)
2376
+ };
2377
+ }
2378
+ async function fetchResourceItem(options, windowId, info, url, signal) {
1874
2379
  const parsedUrl = parseResourceUrl(url);
1875
2380
  if (!parsedUrl) return resourceInvalidRequest(url, `invalid URL: ${url}`);
1876
- const origin = parsedUrl.origin;
1877
- const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
1878
- if (!options.isOriginGranted(origin, grants)) {
1879
- return {
1880
- ok: false,
1881
- url,
1882
- error: "blocked-by-policy",
1883
- code: "denied",
1884
- message: `origin ${origin} not granted`
1885
- };
2381
+ if (options.isOriginGranted && options.getConnectGrants) {
2382
+ const identity = options.resolveIdentity?.(windowId) ?? null;
2383
+ if (!identity) {
2384
+ return {
2385
+ ok: false,
2386
+ url,
2387
+ error: "blocked-by-policy",
2388
+ message: "napplet identity not resolvable for runtime origin policy"
2389
+ };
2390
+ }
2391
+ const origin = parsedUrl.origin;
2392
+ const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
2393
+ if (!options.isOriginGranted(origin, grants)) {
2394
+ return {
2395
+ ok: false,
2396
+ url,
2397
+ error: "blocked-by-policy",
2398
+ message: `origin ${origin} not granted by runtime policy`
2399
+ };
2400
+ }
1886
2401
  }
1887
2402
  try {
1888
2403
  const response = await options.fetch(url, {
1889
- method: init?.method,
1890
- headers: init?.headers ? { ...init.headers } : void 0,
2404
+ method: "GET",
1891
2405
  signal
1892
2406
  });
1893
2407
  const buffer = await response.arrayBuffer();
1894
- const headers = collectResponseHeaders(response);
2408
+ if (info.maxBytes !== void 0 && buffer.byteLength > info.maxBytes) {
2409
+ return { ok: false, url, error: "too-large", message: `resource exceeds ${info.maxBytes} bytes` };
2410
+ }
1895
2411
  const mime = responseMime(response);
1896
2412
  return {
1897
2413
  ok: true,
1898
2414
  url,
1899
2415
  blob: responseBlob(buffer, mime),
1900
- mime,
1901
- status: response.status,
1902
- headers,
1903
- bodyBase64: arrayBufferToBase64(buffer)
2416
+ mime
1904
2417
  };
1905
2418
  } catch (err2) {
1906
2419
  const isAbort = signal.aborted || err2 instanceof Error && (err2.name === "AbortError" || err2.name === "DOMException");
1907
2420
  return {
1908
2421
  ok: false,
1909
2422
  url,
1910
- error: isAbort ? "timeout" : "network-error",
1911
- code: isAbort ? "canceled" : "network-error",
2423
+ error: isAbort ? "timeout" : err2 instanceof ResourceServiceError ? err2.code : "network-error",
1912
2424
  message: err2 instanceof Error ? err2.message : String(err2)
1913
2425
  };
1914
2426
  }
1915
2427
  }
1916
2428
  async function handleBytes(options, state, windowId, msg, send) {
1917
- const { requestId, url, init } = msg;
1918
- const identity = options.resolveIdentity(windowId);
1919
- if (!identity) {
1920
- sendResourceError(send, requestId, "denied", "napplet identity not resolvable", "blocked-by-policy");
1921
- return;
1922
- }
2429
+ const { requestId, url } = msg;
1923
2430
  const controller = new AbortController();
1924
2431
  trackRequest(state, requestId, windowId, controller);
1925
2432
  try {
1926
- const item = await fetchResourceItem(options, identity, url, init, controller.signal);
2433
+ const info = await resolveResourceInfo(options, windowId);
2434
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
2435
+ if (controller.signal.aborted) return;
1927
2436
  if (!item.ok) {
1928
- sendResourceError(send, requestId, item.code, item.message, item.error);
2437
+ sendResourceError(send, requestId, item.message, item.error);
1929
2438
  return;
1930
2439
  }
1931
2440
  send({
1932
2441
  type: "resource.bytes.result",
1933
2442
  id: requestId,
1934
- requestId,
1935
2443
  blob: item.blob,
1936
- mime: item.mime,
1937
- status: item.status,
1938
- headers: item.headers,
1939
- bodyBase64: item.bodyBase64
2444
+ mime: item.mime
1940
2445
  });
2446
+ } catch (error) {
2447
+ if (!controller.signal.aborted) {
2448
+ const failure = classifyResourceFailure(error);
2449
+ sendResourceError(send, requestId, failure.message, failure.error);
2450
+ }
1941
2451
  } finally {
1942
- untrackRequest(state, requestId);
2452
+ untrackRequest(state, windowId, requestId);
1943
2453
  }
1944
2454
  }
1945
2455
  async function handleBytesMany(options, state, windowId, msg, send) {
1946
- const { requestId, urls, init } = msg;
2456
+ const { requestId, urls } = msg;
1947
2457
  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");
2458
+ sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty urls array", "invalid-request");
1949
2459
  return;
1950
2460
  }
1951
- const identity = options.resolveIdentity(windowId);
1952
- if (!identity) {
1953
- sendBytesManyError(send, requestId, "denied", "napplet identity not resolvable", "blocked-by-policy");
2461
+ let info;
2462
+ try {
2463
+ info = await resolveResourceInfo(options, windowId);
2464
+ } catch (error) {
2465
+ const failure = classifyResourceFailure(error);
2466
+ sendBytesManyError(send, requestId, failure.message, failure.error);
2467
+ return;
2468
+ }
2469
+ if (info.maxUrls !== void 0 && urls.length > info.maxUrls) {
2470
+ sendBytesManyError(send, requestId, `resource.bytesMany exceeds ${info.maxUrls} URLs`, "too-large");
1954
2471
  return;
1955
2472
  }
1956
2473
  const controller = new AbortController();
@@ -1958,7 +2475,8 @@ async function handleBytesMany(options, state, windowId, msg, send) {
1958
2475
  try {
1959
2476
  const items = [];
1960
2477
  for (const url of urls) {
1961
- const item = await fetchResourceItem(options, identity, url, init, controller.signal);
2478
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
2479
+ if (controller.signal.aborted) return;
1962
2480
  if (item.ok) {
1963
2481
  items.push({
1964
2482
  url: item.url,
@@ -1971,7 +2489,6 @@ async function handleBytesMany(options, state, windowId, msg, send) {
1971
2489
  url: item.url,
1972
2490
  ok: false,
1973
2491
  error: item.error,
1974
- code: item.code,
1975
2492
  message: item.message
1976
2493
  });
1977
2494
  }
@@ -1979,15 +2496,19 @@ async function handleBytesMany(options, state, windowId, msg, send) {
1979
2496
  send({
1980
2497
  type: "resource.bytesMany.result",
1981
2498
  id: requestId,
1982
- requestId,
1983
2499
  items
1984
2500
  });
2501
+ } catch (error) {
2502
+ if (!controller.signal.aborted) {
2503
+ const failure = classifyResourceFailure(error);
2504
+ sendBytesManyError(send, requestId, failure.message, failure.error);
2505
+ }
1985
2506
  } finally {
1986
- untrackRequest(state, requestId);
2507
+ untrackRequest(state, windowId, requestId);
1987
2508
  }
1988
2509
  }
1989
- function handleCancel(state, requestId) {
1990
- const entry = state.inFlight.get(requestId);
2510
+ function handleCancel(state, windowId, requestId) {
2511
+ const entry = state.inFlight.get(requestKey(windowId, requestId));
1991
2512
  if (entry) {
1992
2513
  entry.controller.abort();
1993
2514
  }
@@ -1995,11 +2516,11 @@ function handleCancel(state, requestId) {
1995
2516
  function destroyWindowRequests(state, windowId) {
1996
2517
  const requestIds = state.perWindow.get(windowId);
1997
2518
  if (!requestIds) return;
1998
- for (const requestId of requestIds) {
1999
- const entry = state.inFlight.get(requestId);
2519
+ for (const key of requestIds) {
2520
+ const entry = state.inFlight.get(key);
2000
2521
  if (entry) {
2001
2522
  entry.controller.abort();
2002
- state.inFlight.delete(requestId);
2523
+ state.inFlight.delete(key);
2003
2524
  }
2004
2525
  }
2005
2526
  state.perWindow.delete(windowId);
@@ -2013,7 +2534,7 @@ function createResourceService(options) {
2013
2534
  const descriptor = {
2014
2535
  name: "resource",
2015
2536
  version: RESOURCE_SERVICE_VERSION,
2016
- description: "NAP-RESOURCE reference service \u2014 shell-proxied authenticated fetch (RESOURCE-01..06)"
2537
+ description: "NAP-RESOURCE service \u2014 runtime-resolved byte transport (RESOURCE-01..06)"
2017
2538
  };
2018
2539
  const handler = {
2019
2540
  descriptor,
@@ -2022,8 +2543,12 @@ function createResourceService(options) {
2022
2543
  case "resource.bytes": {
2023
2544
  const m = message;
2024
2545
  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(() => {
2546
+ if (!requestId) return;
2547
+ if (typeof m.url !== "string") {
2548
+ sendResourceError(send, requestId, "resource.bytes requires a URL", "invalid-request");
2549
+ return;
2550
+ }
2551
+ handleBytes(options, state, windowId, { requestId, url: m.url }, send).catch(() => {
2027
2552
  });
2028
2553
  return;
2029
2554
  }
@@ -2039,14 +2564,14 @@ function createResourceService(options) {
2039
2564
  const m = message;
2040
2565
  const requestId = requestIdFromMessage(m);
2041
2566
  if (!requestId) return;
2042
- handleBytesMany(options, state, windowId, { requestId, urls: m.urls ?? [], init: m.init }, send).catch(() => {
2567
+ handleBytesMany(options, state, windowId, { requestId, urls: m.urls ?? [] }, send).catch(() => {
2043
2568
  });
2044
2569
  return;
2045
2570
  }
2046
2571
  case "resource.cancel": {
2047
2572
  const m = message;
2048
2573
  const requestId = requestIdFromMessage(m);
2049
- if (requestId) handleCancel(state, requestId);
2574
+ if (requestId) handleCancel(state, windowId, requestId);
2050
2575
  return;
2051
2576
  }
2052
2577
  default:
@@ -2067,7 +2592,7 @@ var OUTBOX_DESCRIPTOR = {
2067
2592
  version: OUTBOX_SERVICE_VERSION,
2068
2593
  description: "NAP-OUTBOX outbox-aware relay routing \u2014 getEvent/query/subscribe/publish/resolveRelays"
2069
2594
  };
2070
- function isRecord(value) {
2595
+ function isRecord3(value) {
2071
2596
  return typeof value === "object" && value !== null;
2072
2597
  }
2073
2598
  function stringArray(value) {
@@ -2079,7 +2604,7 @@ function uint(value) {
2079
2604
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
2080
2605
  }
2081
2606
  function sanitizeEventOptions(raw) {
2082
- if (!isRecord(raw)) return void 0;
2607
+ if (!isRecord3(raw)) return void 0;
2083
2608
  const out = {};
2084
2609
  if (typeof raw.author === "string") out.author = raw.author;
2085
2610
  const relays = stringArray(raw.relays);
@@ -2089,7 +2614,7 @@ function sanitizeEventOptions(raw) {
2089
2614
  return Object.keys(out).length > 0 ? out : void 0;
2090
2615
  }
2091
2616
  function sanitizeQueryOptions(raw) {
2092
- if (!isRecord(raw)) return void 0;
2617
+ if (!isRecord3(raw)) return void 0;
2093
2618
  const out = {};
2094
2619
  const authors = stringArray(raw.authors);
2095
2620
  if (authors) out.authors = authors;
@@ -2103,7 +2628,7 @@ function sanitizeQueryOptions(raw) {
2103
2628
  }
2104
2629
  var sanitizeSubscribeOptions = sanitizeQueryOptions;
2105
2630
  function sanitizePublishOptions(raw) {
2106
- if (!isRecord(raw)) return void 0;
2631
+ if (!isRecord3(raw)) return void 0;
2107
2632
  const out = {};
2108
2633
  const relays = stringArray(raw.relays);
2109
2634
  if (relays) out.relays = relays;
@@ -2113,7 +2638,7 @@ function sanitizePublishOptions(raw) {
2113
2638
  return Object.keys(out).length > 0 ? out : void 0;
2114
2639
  }
2115
2640
  function sanitizeTarget(raw) {
2116
- if (!isRecord(raw)) return void 0;
2641
+ if (!isRecord3(raw)) return void 0;
2117
2642
  const out = {};
2118
2643
  const authors = stringArray(raw.authors);
2119
2644
  if (authors) out.authors = authors;
@@ -2775,7 +3300,7 @@ function createRelayPoolOutboxRouter(options) {
2775
3300
  if (!Array.isArray(options.fallbackRelays)) {
2776
3301
  throw new Error("createRelayPoolOutboxRouter: options.fallbackRelays is required");
2777
3302
  }
2778
- const verifyEvent = options.verifyEvent;
3303
+ const verifyEvent2 = options.verifyEvent;
2779
3304
  const ctx = {
2780
3305
  relayPool: options.relayPool,
2781
3306
  loadRelayLists: options.loadRelayLists,
@@ -2784,9 +3309,9 @@ function createRelayPoolOutboxRouter(options) {
2784
3309
  isRelayAllowed: options.isRelayAllowed ?? defaultRelayAllowed,
2785
3310
  defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
2786
3311
  async verify(event) {
2787
- if (!verifyEvent) return true;
3312
+ if (!verifyEvent2) return true;
2788
3313
  try {
2789
- return await verifyEvent(event);
3314
+ return await verifyEvent2(event);
2790
3315
  } catch {
2791
3316
  return false;
2792
3317
  }
@@ -3655,11 +4180,18 @@ function createCvmService(options) {
3655
4180
  sendByWindow.delete(windowId);
3656
4181
  for (const [pubkey, windows] of windowsByServer) {
3657
4182
  windows.delete(windowId);
3658
- if (windows.size === 0) windowsByServer.delete(pubkey);
4183
+ if (windows.size === 0) {
4184
+ windowsByServer.delete(pubkey);
4185
+ void transport.close({ pubkey }).catch(() => {
4186
+ });
4187
+ }
3659
4188
  }
3660
4189
  },
3661
4190
  dispose() {
3662
4191
  eventSub.close();
4192
+ transport.dispose?.();
4193
+ sendByWindow.clear();
4194
+ windowsByServer.clear();
3663
4195
  }
3664
4196
  };
3665
4197
  }
@@ -3848,35 +4380,102 @@ function unsupported(resultType, id) {
3848
4380
  error: `${resultType.replace(".result", "")} unavailable`
3849
4381
  };
3850
4382
  }
4383
+ function createContext(windowId, emit) {
4384
+ return {
4385
+ windowId,
4386
+ emit
4387
+ };
4388
+ }
3851
4389
  function createSerialService(options = {}) {
4390
+ const windows = /* @__PURE__ */ new Map();
4391
+ const currentWindow = (windowId) => {
4392
+ const existing = windows.get(windowId);
4393
+ if (existing) return existing;
4394
+ const record = {
4395
+ connections: /* @__PURE__ */ new Map(),
4396
+ pendingConnections: /* @__PURE__ */ new Set()
4397
+ };
4398
+ windows.set(windowId, record);
4399
+ return record;
4400
+ };
4401
+ const emit = (windowId, record, connection, send, event) => {
4402
+ if (!connection || windows.get(windowId) !== record || connection.state === "closed") return;
4403
+ if (connection.state === "opening") {
4404
+ connection.bufferedEvents.push(event);
4405
+ return;
4406
+ }
4407
+ send({ type: "serial.event", event });
4408
+ };
3852
4409
  return {
3853
4410
  descriptor: SERIAL_DESCRIPTOR,
3854
4411
  handleMessage(windowId, message, send) {
3855
4412
  const id = message.id ?? "";
3856
- const context = { windowId };
4413
+ const record = currentWindow(windowId);
4414
+ const isCurrent = () => windows.get(windowId) === record;
4415
+ const currentSend = (outbound) => {
4416
+ if (isCurrent()) send(outbound);
4417
+ };
3857
4418
  if (message.type === "serial.open") {
3858
4419
  if (!options.open) {
3859
- send(unsupported("serial.open.result", id));
4420
+ currentSend(unsupported("serial.open.result", id));
3860
4421
  return;
3861
4422
  }
3862
4423
  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
- );
4424
+ const connection = { state: "opening", bufferedEvents: [] };
4425
+ record.pendingConnections.add(connection);
4426
+ const context = createContext(windowId, (event) => emit(windowId, record, connection, currentSend, event));
4427
+ let pending;
4428
+ try {
4429
+ pending = Promise.resolve(options.open(serialMessage.request, context));
4430
+ } catch (error) {
4431
+ record.pendingConnections.delete(connection);
4432
+ connection.state = "closed";
4433
+ currentSend({
4434
+ type: "serial.open.result",
4435
+ id,
4436
+ error: errorMessage2(error, "serial request failed")
4437
+ });
4438
+ return;
4439
+ }
4440
+ void pending.then((result2) => {
4441
+ record.pendingConnections.delete(connection);
4442
+ if (!isCurrent() || connection.state === "closed") return;
4443
+ connection.state = "open";
4444
+ record.connections.set(result2.session.id, connection);
4445
+ currentSend({ type: "serial.open.result", id, session: result2.session });
4446
+ if (!isCurrent() || connection.state !== "open") return;
4447
+ for (const event of connection.bufferedEvents) {
4448
+ if (!isCurrent() || connection.state !== "open") break;
4449
+ currentSend({ type: "serial.event", event });
4450
+ }
4451
+ connection.bufferedEvents.length = 0;
4452
+ }).catch((error) => {
4453
+ record.pendingConnections.delete(connection);
4454
+ connection.state = "closed";
4455
+ currentSend({
4456
+ type: "serial.open.result",
4457
+ id,
4458
+ error: errorMessage2(error, "serial request failed")
4459
+ });
4460
+ });
3869
4461
  return;
3870
4462
  }
3871
4463
  if (message.type === "serial.write") {
3872
4464
  if (!options.write) {
3873
- send(unsupported("serial.write.result", id));
4465
+ currentSend(unsupported("serial.write.result", id));
3874
4466
  return;
3875
4467
  }
3876
4468
  const serialMessage = message;
4469
+ const context = createContext(windowId, (event) => emit(
4470
+ windowId,
4471
+ record,
4472
+ record.connections.get(serialMessage.sessionId),
4473
+ currentSend,
4474
+ event
4475
+ ));
3877
4476
  settle2(
3878
4477
  () => options.write(serialMessage.sessionId, serialMessage.data, context),
3879
- send,
4478
+ currentSend,
3880
4479
  (error) => ({ type: "serial.write.result", id, error }),
3881
4480
  () => ({ type: "serial.write.result", id })
3882
4481
  );
@@ -3884,25 +4483,45 @@ function createSerialService(options = {}) {
3884
4483
  }
3885
4484
  if (message.type === "serial.close") {
3886
4485
  if (!options.close) {
3887
- send(unsupported("serial.close.result", id));
4486
+ currentSend(unsupported("serial.close.result", id));
3888
4487
  return;
3889
4488
  }
3890
4489
  const serialMessage = message;
4490
+ const connection = record.connections.get(serialMessage.sessionId);
4491
+ if (connection) {
4492
+ connection.state = "closed";
4493
+ record.connections.delete(serialMessage.sessionId);
4494
+ }
4495
+ const context = createContext(windowId, (event) => emit(
4496
+ windowId,
4497
+ record,
4498
+ connection,
4499
+ currentSend,
4500
+ event
4501
+ ));
3891
4502
  settle2(
3892
4503
  () => options.close(serialMessage.sessionId, serialMessage.reason, context),
3893
- send,
4504
+ currentSend,
3894
4505
  (error) => ({ type: "serial.close.result", id, error }),
3895
4506
  () => ({ type: "serial.close.result", id })
3896
4507
  );
3897
4508
  return;
3898
4509
  }
3899
- send({
4510
+ currentSend({
3900
4511
  type: `${message.type}.error`,
3901
4512
  id,
3902
4513
  error: `Unknown serial method: ${message.type}`
3903
4514
  });
3904
4515
  },
3905
4516
  onWindowDestroyed(windowId) {
4517
+ const record = windows.get(windowId);
4518
+ if (record) {
4519
+ for (const connection of record.connections.values()) connection.state = "closed";
4520
+ for (const connection of record.pendingConnections) connection.state = "closed";
4521
+ record.connections.clear();
4522
+ record.pendingConnections.clear();
4523
+ windows.delete(windowId);
4524
+ }
3906
4525
  options.destroyWindow?.(windowId);
3907
4526
  }
3908
4527
  };
@@ -3937,7 +4556,7 @@ function unsupported2(resultType, id) {
3937
4556
  error: `${resultType.replace(".result", "")} unavailable`
3938
4557
  };
3939
4558
  }
3940
- function createContext(windowId, send) {
4559
+ function createContext2(windowId, send) {
3941
4560
  return {
3942
4561
  windowId,
3943
4562
  emit(event) {
@@ -3950,7 +4569,7 @@ function createBleService(options = {}) {
3950
4569
  descriptor: BLE_DESCRIPTOR,
3951
4570
  handleMessage(windowId, message, send) {
3952
4571
  const id = message.id ?? "";
3953
- const context = createContext(windowId, send);
4572
+ const context = createContext2(windowId, send);
3954
4573
  if (message.type === "ble.open") {
3955
4574
  if (!options.open) {
3956
4575
  send(unsupported2("ble.open.result", id));
@@ -4090,7 +4709,7 @@ function unsupported3(resultType, id) {
4090
4709
  error: `${resultType.replace(".result", "")} unavailable`
4091
4710
  };
4092
4711
  }
4093
- function createContext2(windowId, send) {
4712
+ function createContext3(windowId, send) {
4094
4713
  return {
4095
4714
  windowId,
4096
4715
  emit(event) {
@@ -4103,7 +4722,7 @@ function createWebrtcService(options = {}) {
4103
4722
  descriptor: WEBRTC_DESCRIPTOR,
4104
4723
  handleMessage(windowId, message, send) {
4105
4724
  const id = message.id ?? "";
4106
- const context = createContext2(windowId, send);
4725
+ const context = createContext3(windowId, send);
4107
4726
  if (message.type === "webrtc.open") {
4108
4727
  if (!options.open) {
4109
4728
  send(unsupported3("webrtc.open.result", id));
@@ -4425,7 +5044,7 @@ function createDmService(options) {
4425
5044
  }
4426
5045
  subs.add(subscriptionId);
4427
5046
  }
4428
- function forget(windowId, subscriptionId) {
5047
+ function forget2(windowId, subscriptionId) {
4429
5048
  const subs = ownedSubscriptions.get(windowId);
4430
5049
  if (!subs) return;
4431
5050
  subs.delete(subscriptionId);
@@ -4438,33 +5057,40 @@ function createDmService(options) {
4438
5057
  const requestId = typeof id === "string" ? id : "";
4439
5058
  switch (message.type) {
4440
5059
  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) }));
5060
+ 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
5061
  return;
4443
5062
  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) })
5063
+ void Promise.resolve().then(() => adapter.conversations(message)).then((page) => send({ type: "dm.conversations.result", id: requestId, ...page })).catch(
5064
+ (error) => send({ type: "dm.conversations.result", id: requestId, error: err(error) })
4446
5065
  );
4447
5066
  return;
4448
5067
  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) })
5068
+ void Promise.resolve().then(() => adapter.messages(message)).then((page) => send({ type: "dm.messages.result", id: requestId, ...page })).catch(
5069
+ (error) => send({ type: "dm.messages.result", id: requestId, error: err(error) })
4451
5070
  );
4452
5071
  return;
4453
5072
  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) }));
5073
+ 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
5074
  return;
4456
5075
  case "dm.subscribe": {
4457
5076
  let activeSubscriptionId = "";
4458
- void Promise.resolve(
4459
- adapter.subscribe(message, (dmMessage) => {
5077
+ const pending = [];
5078
+ void Promise.resolve().then(
5079
+ () => adapter.subscribe(message, (dmMessage) => {
4460
5080
  if (activeSubscriptionId.length > 0) {
4461
5081
  send({ type: "dm.message", subscriptionId: activeSubscriptionId, message: dmMessage });
5082
+ } else {
5083
+ pending.push(dmMessage);
4462
5084
  }
4463
5085
  })
4464
5086
  ).then((subscription) => {
4465
5087
  remember(windowId, subscription.subscriptionId);
4466
5088
  activeSubscriptionId = subscription.subscriptionId;
4467
5089
  send({ type: "dm.subscribe.result", id: requestId, ...subscription });
5090
+ for (const dmMessage of pending) {
5091
+ send({ type: "dm.message", subscriptionId: activeSubscriptionId, message: dmMessage });
5092
+ }
5093
+ pending.length = 0;
4468
5094
  }).catch((error) => {
4469
5095
  send({ type: "dm.subscribe.result", id: requestId, error: err(error) });
4470
5096
  });
@@ -4472,14 +5098,14 @@ function createDmService(options) {
4472
5098
  }
4473
5099
  case "dm.unsubscribe": {
4474
5100
  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" });
5101
+ if (typeof subId !== "string" || subId.length === 0 || ownedSubscriptions.get(windowId)?.has(subId) !== true) {
5102
+ send({ type: "dm.unsubscribe.result", id: requestId, error: "subscription not found" });
4477
5103
  return;
4478
5104
  }
4479
- void Promise.resolve(adapter.unsubscribe(subId)).then((ok) => {
4480
- forget(windowId, subId);
5105
+ void Promise.resolve().then(() => adapter.unsubscribe(subId)).then((ok) => {
5106
+ forget2(windowId, subId);
4481
5107
  send({ type: "dm.unsubscribe.result", id: requestId, ...ok });
4482
- }).catch((error) => send({ type: "dm.unsubscribe.result", id: requestId, ok: false, error: err(error) }));
5108
+ }).catch((error) => send({ type: "dm.unsubscribe.result", id: requestId, error: err(error) }));
4483
5109
  return;
4484
5110
  }
4485
5111
  default:
@@ -4500,7 +5126,13 @@ function createDmService(options) {
4500
5126
  }
4501
5127
 
4502
5128
  // src/dm-nip17-adapter.ts
4503
- import { getPublicKey } from "nostr-tools/pure";
5129
+ import * as nip44 from "nostr-tools/nip44";
5130
+ import {
5131
+ getEventHash,
5132
+ getPublicKey,
5133
+ validateEvent,
5134
+ verifyEvent
5135
+ } from "nostr-tools/pure";
4504
5136
  import * as nip17 from "nostr-tools/nip17";
4505
5137
 
4506
5138
  // src/dm-memory-store.ts
@@ -4550,6 +5182,7 @@ var DmMemoryStore = class {
4550
5182
 
4551
5183
  // src/dm-nip17-adapter.ts
4552
5184
  var NIP17_GIFT_WRAP_KIND = 1059;
5185
+ var NIP59_SEAL_KIND = 13;
4553
5186
  var subscriptionCounter = 0;
4554
5187
  function tagsFor(tags, name) {
4555
5188
  return tags.filter((tag) => tag[0] === name && typeof tag[1] === "string").map((tag) => tag[1]);
@@ -4557,110 +5190,163 @@ function tagsFor(tags, name) {
4557
5190
  function nowSeconds() {
4558
5191
  return Math.floor(Date.now() / 1e3);
4559
5192
  }
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);
5193
+ var Nip17DmRuntime = class {
5194
+ constructor(options) {
5195
+ this.options = options;
5196
+ this.ownerPubkey = getPublicKey(options.ownerSecretKey);
5197
+ this.store = options.store ?? new DmMemoryStore();
5198
+ }
5199
+ options;
5200
+ ownerPubkey;
5201
+ store;
5202
+ live = /* @__PURE__ */ new Map();
5203
+ historyLoaded = false;
5204
+ historyLoading = null;
5205
+ adapter() {
5206
+ return {
5207
+ status: () => this.status(),
5208
+ conversations: (query) => this.conversations(query),
5209
+ messages: (query) => this.messages(query),
5210
+ send: (request) => this.send(request),
5211
+ subscribe: (request, onMessage) => this.subscribe(request, onMessage),
5212
+ unsubscribe: (subscriptionId) => this.unsubscribe(subscriptionId),
5213
+ close: () => this.close()
5214
+ };
5215
+ }
5216
+ relaysFor(filters) {
5217
+ return this.options.relays && this.options.relays.length > 0 ? this.options.relays : this.options.relayPool.selectRelayTier(filters);
4566
5218
  }
4567
- function normalizeRumor(rumor, status) {
5219
+ normalizeRumor(rumor, status) {
4568
5220
  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
- );
5221
+ const participants = [.../* @__PURE__ */ new Set([this.ownerPubkey, rumor.pubkey, ...tagsFor(rumor.tags, "p")])];
5222
+ const conversationId = this.store.conversationIdFor(participants);
5223
+ return this.store.upsertMessage({
5224
+ id: rumor.id,
5225
+ conversationId,
5226
+ senderPubkey: rumor.pubkey,
5227
+ createdAt: rumor.created_at,
5228
+ content: rumor.content,
5229
+ status
5230
+ }, participants);
5231
+ }
5232
+ decryptEvent(event) {
5233
+ return JSON.parse(nip44.decrypt(
5234
+ event.content,
5235
+ nip44.getConversationKey(this.options.ownerSecretKey, event.pubkey)
5236
+ ));
5237
+ }
5238
+ unwrapVerified(wrap) {
5239
+ if (wrap.kind !== NIP17_GIFT_WRAP_KIND || !verifyEvent(wrap) || !wrap.tags.some((tag) => tag[0] === "p" && tag[1] === this.ownerPubkey)) return null;
5240
+ try {
5241
+ const seal = this.decryptEvent(wrap);
5242
+ if (!validateEvent(seal) || typeof seal.id !== "string" || typeof seal.sig !== "string" || seal.kind !== NIP59_SEAL_KIND) return null;
5243
+ const signedSeal = seal;
5244
+ if (!verifyEvent(signedSeal)) return null;
5245
+ const rumor = this.decryptEvent(signedSeal);
5246
+ if (!validateEvent(rumor)) return null;
5247
+ const candidate = rumor;
5248
+ if (candidate.kind !== 14 || candidate.pubkey !== signedSeal.pubkey || typeof candidate.id !== "string" || candidate.id !== getEventHash(candidate)) return null;
5249
+ return candidate;
5250
+ } catch {
5251
+ return null;
5252
+ }
4582
5253
  }
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");
5254
+ ingestWrap(wrap, status) {
5255
+ const rumor = this.unwrapVerified(wrap);
5256
+ return rumor ? this.normalizeRumor(rumor, status) : null;
5257
+ }
5258
+ async loadHistory() {
5259
+ if (this.historyLoaded || !this.options.relayPool.query) return;
5260
+ this.historyLoading ??= (async () => {
5261
+ const filters = [{ kinds: [NIP17_GIFT_WRAP_KIND], "#p": [this.ownerPubkey], limit: 1e3 }];
5262
+ const wraps = await this.options.relayPool.query(filters, this.relaysFor(filters));
5263
+ for (const wrap of wraps) this.ingestWrap(wrap, "received");
5264
+ this.historyLoaded = true;
5265
+ })().finally(() => {
5266
+ this.historyLoading = null;
5267
+ });
5268
+ await this.historyLoading;
5269
+ }
5270
+ status() {
5271
+ return {
5272
+ available: this.options.relayPool.isAvailable(),
5273
+ ownerPubkey: this.ownerPubkey,
5274
+ implementations: ["nip17"],
5275
+ capabilities: ["send", "receive", "subscribe", "history"]
5276
+ };
5277
+ }
5278
+ async conversations(query) {
5279
+ await this.loadHistory();
5280
+ return this.store.conversations(query);
5281
+ }
5282
+ async messages(query) {
5283
+ await this.loadHistory();
5284
+ return this.store.messages(query);
5285
+ }
5286
+ async send(request) {
5287
+ if (!this.options.relayPool.isAvailable()) throw new Error("relay unavailable");
5288
+ if (!Array.isArray(request.recipients) || request.recipients.length === 0) throw new Error("invalid recipient");
5289
+ if (request.recipients.some((recipient) => !/^[0-9a-f]{64}$/.test(recipient))) throw new Error("invalid recipient");
5290
+ if (typeof request.content !== "string" || request.content.length === 0) throw new Error("content required");
5291
+ if (this.options.authorizeSend && !await this.options.authorizeSend(request)) throw new Error("forbidden");
5292
+ const recipients = [...new Set(request.recipients)];
5293
+ const wraps = nip17.wrapManyEvents(
5294
+ this.options.ownerSecretKey,
5295
+ recipients.map((publicKey) => ({ publicKey })),
5296
+ request.content
5297
+ );
5298
+ for (const wrap of wraps) await Promise.resolve(this.options.relayPool.publish(wrap));
5299
+ const conversationId = request.conversationId ?? this.store.conversationIdFor([this.ownerPubkey, ...recipients]);
5300
+ const message = this.store.upsertMessage({
5301
+ id: request.clientMessageId ?? wraps[0]?.id ?? `dm-${nowSeconds()}`,
5302
+ conversationId,
5303
+ senderPubkey: this.ownerPubkey,
5304
+ createdAt: nowSeconds(),
5305
+ content: request.content,
5306
+ status: "sent"
5307
+ }, [this.ownerPubkey, ...recipients]);
5308
+ return { ok: true, message };
5309
+ }
5310
+ async subscribe(request, onMessage) {
5311
+ if (!this.options.relayPool.isAvailable()) throw new Error("relay unavailable");
5312
+ await this.loadHistory();
5313
+ const subscriptionId = `dm-nip17-${++subscriptionCounter}`;
5314
+ const filters = [{ kinds: [NIP17_GIFT_WRAP_KIND], "#p": [this.ownerPubkey] }];
5315
+ const sub = { conversationId: request.conversationId, onMessage };
5316
+ this.live.set(subscriptionId, sub);
5317
+ try {
5318
+ const handle = this.options.relayPool.subscribe(filters, (item) => {
5319
+ if (item === "EOSE") return;
5320
+ const message = this.ingestWrap(item, "received");
5321
+ if (!message) return;
5322
+ const active = this.live.get(subscriptionId);
5323
+ if (active && (!active.conversationId || active.conversationId === message.conversationId)) active.onMessage(message);
5324
+ }, this.relaysFor(filters));
5325
+ if (this.live.get(subscriptionId) !== sub) {
5326
+ handle.unsubscribe();
5327
+ throw new Error("subscription cancelled");
4605
5328
  }
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 });
5329
+ sub.handle = handle;
4650
5330
  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();
5331
+ } catch (error) {
5332
+ this.live.delete(subscriptionId);
5333
+ throw error;
4662
5334
  }
4663
- };
5335
+ }
5336
+ unsubscribe(subscriptionId) {
5337
+ const sub = this.live.get(subscriptionId);
5338
+ if (!sub) return { ok: false };
5339
+ sub.handle?.unsubscribe();
5340
+ this.live.delete(subscriptionId);
5341
+ return { ok: true };
5342
+ }
5343
+ close() {
5344
+ for (const sub of this.live.values()) sub.handle?.unsubscribe();
5345
+ this.live.clear();
5346
+ }
5347
+ };
5348
+ function createNip17DmAdapter(options) {
5349
+ return new Nip17DmRuntime(options).adapter();
4664
5350
  }
4665
5351
 
4666
5352
  // src/dm-ndr-adapter.ts
@@ -5041,8 +5727,154 @@ function createCordnDmAdapter(options) {
5041
5727
  }
5042
5728
  };
5043
5729
  }
5730
+
5731
+ // src/fs-service.ts
5732
+ var FsServiceError = class extends Error {
5733
+ constructor(code, message = code) {
5734
+ super(message);
5735
+ this.code = code;
5736
+ this.name = "FsServiceError";
5737
+ }
5738
+ code;
5739
+ };
5740
+ var FS_DESCRIPTOR = {
5741
+ name: "fs",
5742
+ version: "1.0.0",
5743
+ description: "NAP-FS runtime-owned virtual filesystem"
5744
+ };
5745
+ var watchCounter = 0;
5746
+ function fsError(error) {
5747
+ return error instanceof FsServiceError ? error.code : "io-error";
5748
+ }
5749
+ function createFsService(options) {
5750
+ if (!options?.backend) throw new Error("createFsService: options.backend is required");
5751
+ const watches = /* @__PURE__ */ new Map();
5752
+ const watchesByWindow = /* @__PURE__ */ new Map();
5753
+ function ownWatch(watchId, owned) {
5754
+ watches.set(watchId, owned);
5755
+ let ids = watchesByWindow.get(owned.windowId);
5756
+ if (!ids) {
5757
+ ids = /* @__PURE__ */ new Set();
5758
+ watchesByWindow.set(owned.windowId, ids);
5759
+ }
5760
+ ids.add(watchId);
5761
+ }
5762
+ function forgetWatch(watchId) {
5763
+ const owned = watches.get(watchId);
5764
+ if (!owned) return void 0;
5765
+ watches.delete(watchId);
5766
+ const ids = watchesByWindow.get(owned.windowId);
5767
+ ids?.delete(watchId);
5768
+ if (ids?.size === 0) watchesByWindow.delete(owned.windowId);
5769
+ return owned;
5770
+ }
5771
+ function result2(send, type, id, operation, field) {
5772
+ void Promise.resolve().then(operation).then((value) => send({
5773
+ type,
5774
+ id,
5775
+ ...field ? { [field]: value } : {}
5776
+ })).catch((error) => send({ type, id, error: fsError(error) }));
5777
+ }
5778
+ function closeWindowWatches(windowId) {
5779
+ for (const watchId of watchesByWindow.get(windowId) ?? []) {
5780
+ const owned = forgetWatch(watchId);
5781
+ if (owned?.handle) void Promise.resolve(owned.handle.close()).catch(() => void 0);
5782
+ }
5783
+ }
5784
+ return {
5785
+ descriptor: FS_DESCRIPTOR,
5786
+ handleMessage(windowId, message, send) {
5787
+ const input = message;
5788
+ const id = typeof input.id === "string" ? input.id : "";
5789
+ switch (input.type) {
5790
+ case "fs.info":
5791
+ result2(send, "fs.info.result", id, () => options.backend.info(windowId), "info");
5792
+ return;
5793
+ case "fs.pickFile":
5794
+ result2(send, "fs.pickFile.result", id, () => options.backend.pickFile(windowId, input.options), "result");
5795
+ return;
5796
+ case "fs.pickFiles":
5797
+ result2(send, "fs.pickFiles.result", id, () => options.backend.pickFiles(windowId, input.options), "result");
5798
+ return;
5799
+ case "fs.pickDirectory":
5800
+ result2(send, "fs.pickDirectory.result", id, () => options.backend.pickDirectory(windowId, input.options), "result");
5801
+ return;
5802
+ case "fs.pickSaveFile":
5803
+ result2(send, "fs.pickSaveFile.result", id, () => options.backend.pickSaveFile(windowId, input.options), "result");
5804
+ return;
5805
+ case "fs.stat":
5806
+ result2(send, "fs.stat.result", id, () => options.backend.stat(windowId, input.path), "metadata");
5807
+ return;
5808
+ case "fs.list":
5809
+ result2(send, "fs.list.result", id, () => options.backend.list(windowId, input.path), "entries");
5810
+ return;
5811
+ case "fs.read":
5812
+ result2(send, "fs.read.result", id, () => options.backend.read(windowId, input.path, input.options), "result");
5813
+ return;
5814
+ case "fs.write":
5815
+ result2(send, "fs.write.result", id, () => options.backend.write(windowId, input.path, input.data, input.options), "result");
5816
+ return;
5817
+ case "fs.mkdir":
5818
+ result2(send, "fs.mkdir.result", id, () => options.backend.mkdir(windowId, input.path, input.options));
5819
+ return;
5820
+ case "fs.remove":
5821
+ result2(send, "fs.remove.result", id, () => options.backend.remove(windowId, input.path, input.recursive));
5822
+ return;
5823
+ case "fs.move":
5824
+ result2(send, "fs.move.result", id, () => options.backend.move(windowId, input.fromPath, input.toPath));
5825
+ return;
5826
+ case "fs.watch": {
5827
+ const watchId = `fs-watch-${++watchCounter}`;
5828
+ result2(send, "fs.watch.result", id, async () => {
5829
+ const owned = { windowId };
5830
+ ownWatch(watchId, owned);
5831
+ const handle = await options.backend.watch(
5832
+ windowId,
5833
+ input.path,
5834
+ input.options,
5835
+ (change) => {
5836
+ if (watches.get(watchId) !== owned || owned.handle === void 0) return;
5837
+ send({ type: "fs.changed", change: { ...change, watchId } });
5838
+ }
5839
+ );
5840
+ if (watches.get(watchId) !== owned) {
5841
+ await Promise.resolve(handle.close()).catch(() => void 0);
5842
+ throw new FsServiceError("cancelled");
5843
+ }
5844
+ owned.handle = handle;
5845
+ return watchId;
5846
+ }, "watchId");
5847
+ return;
5848
+ }
5849
+ case "fs.unwatch": {
5850
+ const watchId = typeof input.watchId === "string" ? input.watchId : "";
5851
+ const owned = watches.get(watchId);
5852
+ if (!owned || owned.windowId !== windowId) {
5853
+ send({ type: "fs.unwatch.result", id });
5854
+ return;
5855
+ }
5856
+ const forgotten = forgetWatch(watchId);
5857
+ result2(send, "fs.unwatch.result", id, () => forgotten?.handle?.close());
5858
+ return;
5859
+ }
5860
+ default:
5861
+ return;
5862
+ }
5863
+ },
5864
+ onWindowDestroyed(windowId) {
5865
+ closeWindowWatches(windowId);
5866
+ options.backend.onWindowDestroyed?.(windowId);
5867
+ },
5868
+ dispose() {
5869
+ for (const windowId of watchesByWindow.keys()) closeWindowWatches(windowId);
5870
+ options.backend.close?.();
5871
+ }
5872
+ };
5873
+ }
5044
5874
  export {
5045
5875
  DmMemoryStore,
5876
+ FsServiceError,
5877
+ ResourceServiceError,
5046
5878
  createBleService,
5047
5879
  createBrowserMediaBridge,
5048
5880
  createCacheService,
@@ -5055,6 +5887,7 @@ export {
5055
5887
  createCountService,
5056
5888
  createCvmService,
5057
5889
  createDmService,
5890
+ createFsService,
5058
5891
  createHttpUploader,
5059
5892
  createIdentityService,
5060
5893
  createIntentService,
@@ -5075,6 +5908,8 @@ export {
5075
5908
  createThemeService,
5076
5909
  createUploadService,
5077
5910
  createWebrtcService,
5078
- manifestToIntentCatalogEntry
5911
+ manifestToIntentCatalogEntry,
5912
+ resolveConfigValues,
5913
+ validateConfigSchema
5079
5914
  };
5080
5915
  //# sourceMappingURL=index.js.map