@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/README.md +29 -10
- 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 +227 -238
- package/dist/index.js +1220 -385
- 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,202 +1693,626 @@ 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;
|
|
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
|
-
|
|
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
|
-
if (typeof options?.fetch !== "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
|
|
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
|
-
|
|
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(
|
|
2296
|
+
state.perWindow.get(windowId).add(key);
|
|
1777
2297
|
}
|
|
1778
|
-
function untrackRequest(state, requestId) {
|
|
1779
|
-
const
|
|
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(
|
|
1782
|
-
state.perWindow.get(entry.windowId)?.delete(
|
|
2302
|
+
state.inFlight.delete(key);
|
|
2303
|
+
state.perWindow.get(entry.windowId)?.delete(key);
|
|
1783
2304
|
}
|
|
1784
2305
|
}
|
|
1785
|
-
function sendResourceError(send, requestId,
|
|
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,
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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:
|
|
1890
|
-
headers: init?.headers ? { ...init.headers } : void 0,
|
|
2404
|
+
method: "GET",
|
|
1891
2405
|
signal
|
|
1892
2406
|
});
|
|
1893
2407
|
const buffer = await response.arrayBuffer();
|
|
1894
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
2456
|
+
const { requestId, urls } = msg;
|
|
1947
2457
|
if (!Array.isArray(urls) || urls.length === 0 || urls.some((url) => typeof url !== "string")) {
|
|
1948
|
-
sendBytesManyError(send, requestId, "
|
|
2458
|
+
sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty urls array", "invalid-request");
|
|
1949
2459
|
return;
|
|
1950
2460
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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,
|
|
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
|
|
1999
|
-
const entry = state.inFlight.get(
|
|
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(
|
|
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
|
|
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
|
|
2026
|
-
|
|
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 ?? []
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 (!
|
|
3312
|
+
if (!verifyEvent2) return true;
|
|
2788
3313
|
try {
|
|
2789
|
-
return await
|
|
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)
|
|
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
|
|
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
|
-
|
|
4420
|
+
currentSend(unsupported("serial.open.result", id));
|
|
3860
4421
|
return;
|
|
3861
4422
|
}
|
|
3862
4423
|
const serialMessage = message;
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
4459
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
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
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
};
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
|
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
|