@kehto/shell 0.16.6 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -961,36 +961,6 @@ var audioManager = {
961
961
  }
962
962
  };
963
963
 
964
- // src/keys-forwarder.ts
965
- function createKeysForwarder(deps) {
966
- const target = deps.target ?? (typeof window !== "undefined" ? window : new EventTarget());
967
- const listener = (ev) => {
968
- const ke = ev;
969
- const entries = deps.sessionRegistry.getAllEntries();
970
- for (const entry of entries) {
971
- if (!deps.hasKeysForwardCap(entry.pubkey)) continue;
972
- const iframe = deps.originRegistry.getIframeWindow(entry.windowId);
973
- if (!iframe) continue;
974
- const envelope = {
975
- type: "keys.forward",
976
- key: ke.key ?? "",
977
- code: ke.code ?? "",
978
- ctrl: ke.ctrlKey ?? false,
979
- alt: ke.altKey ?? false,
980
- shift: ke.shiftKey ?? false,
981
- meta: ke.metaKey ?? false
982
- };
983
- iframe.postMessage(envelope, "*");
984
- }
985
- };
986
- target.addEventListener("keydown", listener);
987
- return {
988
- destroy() {
989
- target.removeEventListener("keydown", listener);
990
- }
991
- };
992
- }
993
-
994
964
  // src/shell-init.ts
995
965
  var NAP_DOMAINS = [
996
966
  "identity",
@@ -1169,23 +1139,6 @@ function createShellBridge(hooks) {
1169
1139
  win.postMessage(envelope, "*");
1170
1140
  }
1171
1141
  }
1172
- let keysForwarder = null;
1173
- if (typeof window !== "undefined") {
1174
- try {
1175
- keysForwarder = createKeysForwarder({
1176
- originRegistry,
1177
- sessionRegistry,
1178
- hasKeysForwardCap: (pubkey) => {
1179
- const entry = sessionRegistry.getEntry(pubkey);
1180
- if (!entry) return false;
1181
- const acl = aclStore.getEntry(entry.pubkey, entry.dTag, entry.aggregateHash);
1182
- return acl?.capabilities.includes("keys:forward") ?? false;
1183
- }
1184
- });
1185
- } catch {
1186
- keysForwarder = null;
1187
- }
1188
- }
1189
1142
  return {
1190
1143
  handleMessage(event) {
1191
1144
  const sourceWindow = event.source;
@@ -1217,7 +1170,6 @@ function createShellBridge(hooks) {
1217
1170
  runtime.injectEvent(topic, payload);
1218
1171
  },
1219
1172
  destroy() {
1220
- keysForwarder?.destroy();
1221
1173
  runtime.destroy();
1222
1174
  },
1223
1175
  registerConsentHandler(handler) {
@@ -1255,14 +1207,14 @@ function scriptJson(value) {
1255
1207
  return JSON.stringify(value).replace(/</g, "\\u003c");
1256
1208
  }
1257
1209
  function renderNappletNamespacePrelude(options) {
1258
- const domains = uniqueBareDomains(options.domains);
1210
+ const domains = uniqueBareDomains(["shell", ...options.domains]);
1259
1211
  return `<script data-kehto-nip5d-injection>(${nappletNamespacePrelude.toString()})(${scriptJson(domains)});</script>`;
1260
1212
  }
1261
1213
  function injectNappletNamespacePrelude(html, options) {
1262
1214
  const prelude = renderNappletNamespacePrelude(options);
1263
1215
  const cspMeta = /(<meta\s+http-equiv=["']Content-Security-Policy["'][^>]*>)/i;
1264
1216
  if (cspMeta.test(html)) {
1265
- return html.replace(cspMeta, `$1${prelude}`);
1217
+ return html.replace(cspMeta, (match) => `${match}${prelude}`);
1266
1218
  }
1267
1219
  if (/<head[^>]*>/i.test(html)) {
1268
1220
  return html.replace(/<head[^>]*>/i, (open) => `${open}${prelude}`);
@@ -1349,6 +1301,67 @@ function nappletNamespacePrelude(domains) {
1349
1301
  }
1350
1302
  };
1351
1303
  }
1304
+ function makeShell() {
1305
+ const emptyServices = Object.freeze([]);
1306
+ const readyHandlers = /* @__PURE__ */ new Set();
1307
+ let environment;
1308
+ let resolveReady = () => void 0;
1309
+ const ready = new Promise((resolve) => {
1310
+ resolveReady = resolve;
1311
+ });
1312
+ const off = listen((event) => {
1313
+ if (!isParentMessage(event) || environment) return;
1314
+ const incoming = event.data;
1315
+ if (typeof incoming !== "object" || incoming === null) return;
1316
+ const message = incoming;
1317
+ if (message.type !== "shell.init") return;
1318
+ const services = Object.freeze(
1319
+ Array.isArray(message.services) ? message.services.filter((service) => typeof service === "string") : []
1320
+ );
1321
+ environment = Object.freeze({
1322
+ capabilities: message.capabilities ?? {},
1323
+ services
1324
+ });
1325
+ off();
1326
+ resolveReady(environment);
1327
+ for (const handler of readyHandlers) handler(environment);
1328
+ readyHandlers.clear();
1329
+ });
1330
+ function supports(domain, protocol) {
1331
+ if (!environment || typeof environment.capabilities !== "object" || environment.capabilities === null) {
1332
+ return false;
1333
+ }
1334
+ const capabilities = environment.capabilities;
1335
+ if (protocol !== void 0) {
1336
+ const protocols = capabilities.protocols;
1337
+ if (typeof protocols === "object" && protocols !== null) {
1338
+ const supported = protocols[domain];
1339
+ if (Array.isArray(supported) && supported.includes(protocol)) return true;
1340
+ }
1341
+ return Array.isArray(capabilities.naps) && capabilities.naps.includes(`${domain}:${protocol}`);
1342
+ }
1343
+ return Array.isArray(capabilities.domains) && capabilities.domains.includes(domain) || Array.isArray(capabilities.naps) && capabilities.naps.includes(domain);
1344
+ }
1345
+ const shell2 = {
1346
+ ready: () => ready,
1347
+ supports,
1348
+ get services() {
1349
+ return environment?.services ?? emptyServices;
1350
+ },
1351
+ set services(_services) {
1352
+ },
1353
+ onReady(handler) {
1354
+ if (environment) {
1355
+ handler(environment);
1356
+ return subscriptionHandle(() => void 0);
1357
+ }
1358
+ readyHandlers.add(handler);
1359
+ return subscriptionHandle(() => readyHandlers.delete(handler));
1360
+ }
1361
+ };
1362
+ post({ type: "shell.ready" });
1363
+ return shell2;
1364
+ }
1352
1365
  function domainEventHandle(eventType, callback, select = (message) => message.event) {
1353
1366
  const off = listen((event) => {
1354
1367
  if (!isParentMessage(event)) return;
@@ -1524,30 +1537,177 @@ function nappletNamespacePrelude(domains) {
1524
1537
  }
1525
1538
  };
1526
1539
  }
1540
+ const keyActionCallbacks = /* @__PURE__ */ new Map();
1541
+ const keyBindingActions = /* @__PURE__ */ new Map();
1542
+ const modifierKeys = /* @__PURE__ */ new Set(["Control", "Alt", "Shift", "Meta"]);
1543
+ const reservedKeyNames = /* @__PURE__ */ new Set(["Tab", "Escape"]);
1544
+ const nonTextInputTypes = /* @__PURE__ */ new Set([
1545
+ "button",
1546
+ "checkbox",
1547
+ "color",
1548
+ "file",
1549
+ "hidden",
1550
+ "image",
1551
+ "radio",
1552
+ "range",
1553
+ "reset",
1554
+ "submit"
1555
+ ]);
1556
+ let keysSmartForwardingInstalled = false;
1557
+ function normalizeKeyName(key) {
1558
+ if (typeof key !== "string") return "";
1559
+ return key.length === 1 ? key.toUpperCase() : key;
1560
+ }
1561
+ function keyComboKey(value) {
1562
+ return `${value.ctrl}|${value.alt}|${value.shift}|${value.meta}|${normalizeKeyName(value.key)}`;
1563
+ }
1564
+ function bindingComboKey(binding) {
1565
+ if (typeof binding !== "string" || binding.trim() === "") return null;
1566
+ const parts = binding.split("+");
1567
+ const key = parts.pop()?.trim();
1568
+ if (!key) return null;
1569
+ const combo = { ctrl: false, alt: false, shift: false, meta: false, key };
1570
+ for (const raw of parts) {
1571
+ const token = raw.trim().toLowerCase();
1572
+ if (!token) continue;
1573
+ if (token === "ctrl" || token === "control") combo.ctrl = true;
1574
+ else if (token === "alt" || token === "option") combo.alt = true;
1575
+ else if (token === "shift") combo.shift = true;
1576
+ else if (token === "meta" || token === "cmd" || token === "command" || token === "win" || token === "super") combo.meta = true;
1577
+ else return null;
1578
+ }
1579
+ return keyComboKey(combo);
1580
+ }
1581
+ function eventComboKey(event) {
1582
+ const key = typeof event.key === "string" ? event.key : "";
1583
+ if (!key) return null;
1584
+ return keyComboKey({
1585
+ ctrl: Boolean(event.ctrlKey),
1586
+ alt: Boolean(event.altKey),
1587
+ shift: Boolean(event.shiftKey),
1588
+ meta: Boolean(event.metaKey),
1589
+ key
1590
+ });
1591
+ }
1592
+ function isTextEntryTarget(value) {
1593
+ if (!value || typeof value !== "object") return false;
1594
+ const targetNode = value;
1595
+ if (targetNode.isContentEditable === true) return true;
1596
+ const tagName = typeof targetNode.tagName === "string" ? targetNode.tagName.toLowerCase() : "";
1597
+ if (tagName === "textarea" || tagName === "select") return true;
1598
+ if (tagName === "input") {
1599
+ const type = typeof targetNode.type === "string" && targetNode.type ? targetNode.type.toLowerCase() : "text";
1600
+ return !nonTextInputTypes.has(type);
1601
+ }
1602
+ if (typeof targetNode.closest === "function") {
1603
+ try {
1604
+ return Boolean(targetNode.closest('[contenteditable=""],[contenteditable="true"],[contenteditable="plaintext-only"]'));
1605
+ } catch {
1606
+ return false;
1607
+ }
1608
+ }
1609
+ return false;
1610
+ }
1611
+ function rememberKeyBinding(actionId, binding) {
1612
+ if (typeof actionId !== "string") return;
1613
+ const combo = bindingComboKey(binding);
1614
+ if (combo) keyBindingActions.set(combo, actionId);
1615
+ }
1616
+ function removeKeyBindingsForAction(actionId) {
1617
+ for (const [combo, boundActionId] of keyBindingActions) {
1618
+ if (boundActionId === actionId) keyBindingActions.delete(combo);
1619
+ }
1620
+ }
1621
+ function replaceKeyBindings(bindings) {
1622
+ keyBindingActions.clear();
1623
+ if (!Array.isArray(bindings)) return;
1624
+ for (const binding of bindings) {
1625
+ if (!binding || typeof binding !== "object") continue;
1626
+ const entry = binding;
1627
+ rememberKeyBinding(entry.actionId, entry.key);
1628
+ }
1629
+ }
1630
+ function notifyKeyAction(actionId) {
1631
+ for (const callback of keyActionCallbacks.get(actionId) ?? []) callback();
1632
+ }
1633
+ function forwardKeyEvent(event = {}) {
1634
+ fire({
1635
+ type: "keys.forward",
1636
+ key: typeof event.key === "string" ? event.key : "",
1637
+ code: typeof event.code === "string" ? event.code : "",
1638
+ ctrl: Boolean(event.ctrl ?? event.ctrlKey),
1639
+ alt: Boolean(event.alt ?? event.altKey),
1640
+ shift: Boolean(event.shift ?? event.shiftKey),
1641
+ meta: Boolean(event.meta ?? event.metaKey)
1642
+ });
1643
+ }
1644
+ function installKeysSmartForwarding() {
1645
+ if (keysSmartForwardingInstalled) return;
1646
+ keysSmartForwardingInstalled = true;
1647
+ listen((event) => {
1648
+ if (!isParentMessage(event)) return;
1649
+ const msg = event.data;
1650
+ if (typeof msg === "object" && msg !== null && msg.type === "keys.bindings") {
1651
+ replaceKeyBindings(msg.bindings);
1652
+ }
1653
+ });
1654
+ const documentLike = target.document;
1655
+ const listenerTarget = typeof documentLike?.addEventListener === "function" ? documentLike : target;
1656
+ if (typeof listenerTarget.addEventListener !== "function") return;
1657
+ listenerTarget.addEventListener("keydown", (event) => {
1658
+ const keyEvent = event;
1659
+ if (keyEvent.isComposing) return;
1660
+ if (typeof keyEvent.key === "string" && modifierKeys.has(keyEvent.key)) return;
1661
+ if (isTextEntryTarget(keyEvent.target ?? documentLike?.activeElement)) return;
1662
+ const combo = eventComboKey(keyEvent);
1663
+ const actionId = combo ? keyBindingActions.get(combo) : void 0;
1664
+ const isReserved = typeof keyEvent.key === "string" && reservedKeyNames.has(keyEvent.key);
1665
+ if (actionId && !isReserved) {
1666
+ keyEvent.preventDefault?.();
1667
+ notifyKeyAction(actionId);
1668
+ return;
1669
+ }
1670
+ forwardKeyEvent(keyEvent);
1671
+ });
1672
+ }
1527
1673
  function makeKeys() {
1674
+ installKeysSmartForwarding();
1528
1675
  return {
1529
- forward: (event = {}) => fire({
1530
- type: "keys.forward",
1531
- key: typeof event.key === "string" ? event.key : "",
1532
- code: typeof event.code === "string" ? event.code : "",
1533
- ctrl: Boolean(event.ctrl ?? event.ctrlKey),
1534
- alt: Boolean(event.alt ?? event.altKey),
1535
- shift: Boolean(event.shift ?? event.shiftKey),
1536
- meta: Boolean(event.meta ?? event.metaKey)
1537
- }),
1676
+ forward: forwardKeyEvent,
1538
1677
  registerAction: (action) => request(
1539
1678
  { type: "keys.registerAction", action },
1540
1679
  "keys.registerAction.result",
1541
1680
  resultOrError
1542
- ),
1543
- unregisterAction: (actionId) => fire({ type: "keys.unregisterAction", actionId }),
1681
+ ).then((result) => {
1682
+ const actionRecord = action && typeof action === "object" ? action : {};
1683
+ const resultRecord = result;
1684
+ rememberKeyBinding(
1685
+ typeof resultRecord.actionId === "string" ? resultRecord.actionId : actionRecord.id,
1686
+ resultRecord.binding
1687
+ );
1688
+ return result;
1689
+ }),
1690
+ unregisterAction: (actionId) => {
1691
+ removeKeyBindingsForAction(actionId);
1692
+ fire({ type: "keys.unregisterAction", actionId });
1693
+ },
1544
1694
  onAction(actionId, callback) {
1695
+ let callbacks = keyActionCallbacks.get(actionId);
1696
+ if (!callbacks) {
1697
+ callbacks = /* @__PURE__ */ new Set();
1698
+ keyActionCallbacks.set(actionId, callbacks);
1699
+ }
1700
+ callbacks.add(callback);
1545
1701
  const off = listen((event) => {
1546
1702
  if (!isParentMessage(event)) return;
1547
1703
  const msg = event.data;
1548
1704
  if (typeof msg === "object" && msg !== null && msg.type === "keys.action" && msg.actionId === actionId) callback();
1549
1705
  });
1550
- return subscriptionHandle(off);
1706
+ return subscriptionHandle(() => {
1707
+ off();
1708
+ callbacks.delete(callback);
1709
+ if (callbacks.size === 0) keyActionCallbacks.delete(actionId);
1710
+ });
1551
1711
  }
1552
1712
  };
1553
1713
  }
@@ -1962,7 +2122,9 @@ function nappletNamespacePrelude(domains) {
1962
2122
  }
1963
2123
  };
1964
2124
  }
2125
+ const shell = makeShell();
1965
2126
  function makeDomain(domain, existing) {
2127
+ if (domain === "shell") return shell;
1966
2128
  if (existing && typeof existing === "object" && Object.keys(existing).length > 0) return existing;
1967
2129
  switch (domain) {
1968
2130
  case "relay":
@@ -2138,7 +2300,6 @@ export {
2138
2300
  buildShellCapabilities,
2139
2301
  createEnforceGate,
2140
2302
  createIdentityProxy,
2141
- createKeysForwarder,
2142
2303
  createKeysProxy,
2143
2304
  createMediaProxy,
2144
2305
  createNapEnforceGate,