@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/README.md +28 -9
- package/dist/cvm-nostr-transport.d.ts +2 -2
- package/dist/cvm-nostr-transport.js +17 -12
- package/dist/cvm-nostr-transport.js.map +1 -1
- package/dist/{cvm-service-DKIzbiDw.d.ts → cvm-service-t5syG4tU.d.ts} +2 -0
- package/dist/index.d.ts +197 -213
- package/dist/index.js +1195 -366
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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({
|
|
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
|
|
1504
|
-
|
|
1505
|
-
|
|
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: "
|
|
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
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
1614
|
-
var
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
1620
|
-
|
|
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 (
|
|
1624
|
-
|
|
1625
|
-
ok: false,
|
|
1626
|
-
|
|
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 (
|
|
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
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
1645
|
-
|
|
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
|
-
|
|
1648
|
-
|
|
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 (
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
-
|
|
1732
|
-
|
|
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
|
-
|
|
1736
|
-
|
|
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
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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: [
|
|
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
|
-
|
|
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(
|
|
2287
|
+
state.perWindow.get(windowId).add(key);
|
|
1777
2288
|
}
|
|
1778
|
-
function untrackRequest(state, requestId) {
|
|
1779
|
-
const
|
|
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(
|
|
1782
|
-
state.perWindow.get(entry.windowId)?.delete(
|
|
2293
|
+
state.inFlight.delete(key);
|
|
2294
|
+
state.perWindow.get(entry.windowId)?.delete(key);
|
|
1783
2295
|
}
|
|
1784
2296
|
}
|
|
1785
|
-
function sendResourceError(send, requestId,
|
|
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,
|
|
1796
|
-
sendResourceError(send, requestId,
|
|
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
|
-
|
|
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:
|
|
1890
|
-
headers: init?.headers ? { ...init.headers } : void 0,
|
|
2388
|
+
method: "GET",
|
|
1891
2389
|
signal
|
|
1892
2390
|
});
|
|
1893
2391
|
const buffer = await response.arrayBuffer();
|
|
1894
|
-
|
|
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
|
|
2413
|
+
const { requestId, url } = msg;
|
|
1918
2414
|
const identity = options.resolveIdentity(windowId);
|
|
1919
2415
|
if (!identity) {
|
|
1920
|
-
sendResourceError(send, requestId, "
|
|
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
|
|
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.
|
|
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
|
|
2445
|
+
const { requestId, urls } = msg;
|
|
1947
2446
|
if (!Array.isArray(urls) || urls.length === 0 || urls.some((url) => typeof url !== "string")) {
|
|
1948
|
-
sendBytesManyError(send, requestId, "
|
|
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, "
|
|
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,
|
|
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
|
|
1999
|
-
const entry = state.inFlight.get(
|
|
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(
|
|
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
|
|
2026
|
-
|
|
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 ?? []
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 (!
|
|
3306
|
+
if (!verifyEvent2) return true;
|
|
2788
3307
|
try {
|
|
2789
|
-
return await
|
|
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)
|
|
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
|
|
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
|
-
|
|
4414
|
+
currentSend(unsupported("serial.open.result", id));
|
|
3860
4415
|
return;
|
|
3861
4416
|
}
|
|
3862
4417
|
const serialMessage = message;
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
4459
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
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
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
};
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4653
|
-
|
|
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
|