@kehto/shell 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -976,6 +976,7 @@ var audioManager = {
976
976
 
977
977
  // src/shell-init.ts
978
978
  var RUNTIME_NATIVE_DOMAINS = [
979
+ "shell",
979
980
  "relay",
980
981
  "identity",
981
982
  "storage",
@@ -1000,7 +1001,8 @@ var SERVICE_ONLY_DOMAINS = [
1000
1001
  "webrtc",
1001
1002
  "dm",
1002
1003
  "count",
1003
- "fs"
1004
+ "fs",
1005
+ "gamepad"
1004
1006
  ];
1005
1007
  function buildShellCapabilities(hooks) {
1006
1008
  const disabled = new Set(hooks.capabilities?.disabledDomains ?? []);
@@ -1096,14 +1098,21 @@ function handleShellReady({
1096
1098
  if (state.initSent.get(sourceWindow) === sourceRegistrationId) {
1097
1099
  return;
1098
1100
  }
1099
- const identity = resolveNip5dIdentity(hooks, windowId, sourceWindow);
1100
- if (!identity) return;
1101
- registerNip5dSessionIfNeeded({ origin, runtime, state, sourceRegistrationId, windowId, identity });
1102
- const environment = originRegistry.getEnvironment(sourceWindow) ?? resolveShellEnvironment(hooks, identity);
1103
- state.environments.set(windowId, environment);
1101
+ const environment = registerSourceSession({ hooks, origin, runtime, state, sourceRegistrationId, sourceWindow, windowId });
1102
+ if (!environment?.capabilities.domains.includes("shell")) return;
1104
1103
  postShellInit(sourceWindow, environment);
1105
1104
  state.initSent.set(sourceWindow, sourceRegistrationId);
1106
1105
  }
1106
+ function registerSourceSession(options) {
1107
+ const { hooks, origin, runtime, state, sourceRegistrationId, sourceWindow, windowId } = options;
1108
+ const identity = resolveNip5dIdentity(hooks, windowId, sourceWindow, runtime.sessionRegistry.getEntryByWindowId(windowId));
1109
+ if (!identity) return null;
1110
+ registerNip5dSessionIfNeeded({ origin, runtime, state, sourceRegistrationId, windowId, identity });
1111
+ const existing = state.environments.get(windowId);
1112
+ const environment = originRegistry.getEnvironment(sourceWindow) ?? existing ?? resolveShellEnvironment(hooks, identity);
1113
+ state.environments.set(windowId, environment);
1114
+ return environment;
1115
+ }
1107
1116
  function registerNip5dSessionIfNeeded({
1108
1117
  origin,
1109
1118
  runtime,
@@ -1118,9 +1127,14 @@ function registerNip5dSessionIfNeeded({
1118
1127
  return;
1119
1128
  }
1120
1129
  if (existing && previousRegistration !== void 0) {
1130
+ runtime.destroyWindow(windowId);
1121
1131
  runtime.sessionRegistry.unregister(windowId);
1132
+ state.environments.delete(windowId);
1133
+ }
1134
+ if (existing && previousRegistration === void 0) {
1135
+ state.sessionRegistration.set(windowId, sourceRegistrationId);
1136
+ return;
1122
1137
  }
1123
- if (existing && previousRegistration === void 0) return;
1124
1138
  const entry = {
1125
1139
  pubkey: "",
1126
1140
  windowId,
@@ -1135,7 +1149,7 @@ function registerNip5dSessionIfNeeded({
1135
1149
  runtime.sessionRegistry.register(windowId, entry);
1136
1150
  state.sessionRegistration.set(windowId, sourceRegistrationId);
1137
1151
  }
1138
- function resolveNip5dIdentity(hooks, windowId, sourceWindow) {
1152
+ function resolveNip5dIdentity(hooks, windowId, sourceWindow, existingSession) {
1139
1153
  const hookIdentity = hooks.onNip5dIframeCreate?.(windowId);
1140
1154
  if (hookIdentity !== null && hookIdentity !== void 0) {
1141
1155
  return Object.freeze({
@@ -1145,7 +1159,8 @@ function resolveNip5dIdentity(hooks, windowId, sourceWindow) {
1145
1159
  }
1146
1160
  const originIdentity = originRegistry.getIdentity(sourceWindow);
1147
1161
  if (!originIdentity) {
1148
- return null;
1162
+ if (existingSession?.provenance !== "nip-5d" || typeof existingSession.dTag !== "string" || typeof existingSession.aggregateHash !== "string") return null;
1163
+ return Object.freeze({ dTag: existingSession.dTag, aggregateHash: existingSession.aggregateHash });
1149
1164
  }
1150
1165
  return Object.freeze({
1151
1166
  dTag: originIdentity.dTag,
@@ -1208,6 +1223,15 @@ function createShellBridge(hooks) {
1208
1223
  }
1209
1224
  const msg = event.data;
1210
1225
  if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
1226
+ if (!registerSourceSession({
1227
+ hooks,
1228
+ origin: event.origin,
1229
+ runtime,
1230
+ state: shellReadyState,
1231
+ sourceRegistrationId: originRegistry.getRegistrationId(sourceWindow) ?? 0,
1232
+ sourceWindow,
1233
+ windowId
1234
+ })) return;
1211
1235
  if (msg.type === "shell.ready") {
1212
1236
  handleShellReady({
1213
1237
  hooks,
@@ -1246,6 +1270,188 @@ function createShellBridge(hooks) {
1246
1270
  };
1247
1271
  }
1248
1272
 
1273
+ // src/gamepad-policy.ts
1274
+ var CONTROLLER_FEATURES = ["gamepad", "hid", "usb"];
1275
+ function denyNappletControllerAccess(frame) {
1276
+ const remaining = (frame.getAttribute("allow") ?? "").split(";").map((entry) => entry.trim()).filter((entry) => entry && !CONTROLLER_FEATURES.some((feature) => entry === feature || entry.startsWith(`${feature} `)));
1277
+ frame.setAttribute("allow", [...remaining, ...CONTROLLER_FEATURES.map((feature) => `${feature} 'none'`)].join("; "));
1278
+ }
1279
+
1280
+ // src/intent-uri.ts
1281
+ function createIntentUriNormalizer() {
1282
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1283
+ const fail = (message) => {
1284
+ throw new TypeError(message);
1285
+ };
1286
+ const decode = (value, label) => {
1287
+ try {
1288
+ return decodeURIComponent(value);
1289
+ } catch {
1290
+ return fail(`Intent URI has malformed percent-encoding in ${label}`);
1291
+ }
1292
+ };
1293
+ const decodeUtf8 = (bytes) => {
1294
+ try {
1295
+ return new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes));
1296
+ } catch {
1297
+ return fail("Intent URI fragment contains invalid UTF-8");
1298
+ }
1299
+ };
1300
+ const decodeHint = (fragment) => {
1301
+ if (!fragment.startsWith("naddr") || fragment !== fragment.toLowerCase()) {
1302
+ return fail("Intent URI fragment must be a bare lowercase naddr");
1303
+ }
1304
+ const separator = fragment.lastIndexOf("1");
1305
+ if (separator <= 0 || separator + 7 > fragment.length) fail("Intent URI fragment is not a valid naddr");
1306
+ const alphabet = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1307
+ const values = [];
1308
+ for (const character of fragment.slice(separator + 1)) {
1309
+ const value = alphabet.indexOf(character);
1310
+ if (value < 0) fail("Intent URI fragment is not a valid naddr");
1311
+ values.push(value);
1312
+ }
1313
+ const polymod = (input) => {
1314
+ let checksum = 1;
1315
+ for (const value of input) {
1316
+ const top = checksum >>> 25;
1317
+ checksum = (checksum & 33554431) << 5 ^ value;
1318
+ if (top & 1) checksum ^= 996825010;
1319
+ if (top & 2) checksum ^= 642813549;
1320
+ if (top & 4) checksum ^= 513874426;
1321
+ if (top & 8) checksum ^= 1027748829;
1322
+ if (top & 16) checksum ^= 705979059;
1323
+ }
1324
+ return checksum;
1325
+ };
1326
+ const hrp = fragment.slice(0, separator);
1327
+ const hrpValues = [...hrp].map((character) => character.charCodeAt(0) >>> 5).concat([0], [...hrp].map((character) => character.charCodeAt(0) & 31));
1328
+ if (hrp !== "naddr" || polymod(hrpValues.concat(values)) !== 1) fail("Intent URI fragment is not a valid naddr");
1329
+ const words = values.slice(0, -6);
1330
+ const bytes = [];
1331
+ let accumulator = 0;
1332
+ let bits = 0;
1333
+ for (const word of words) {
1334
+ accumulator = accumulator << 5 | word;
1335
+ bits += 5;
1336
+ while (bits >= 8) {
1337
+ bits -= 8;
1338
+ bytes.push(accumulator >>> bits & 255);
1339
+ }
1340
+ }
1341
+ if (bits >= 5 || (accumulator << 8 - bits & 255) !== 0) fail("Intent URI fragment is not a valid naddr");
1342
+ let index = 0;
1343
+ let identifier;
1344
+ let pubkey;
1345
+ let kind;
1346
+ const relays = [];
1347
+ while (index < bytes.length) {
1348
+ if (index + 2 > bytes.length) fail("Intent URI fragment is not a valid naddr");
1349
+ const type = bytes[index++];
1350
+ const length = bytes[index++];
1351
+ if (index + length > bytes.length) fail("Intent URI fragment is not a valid naddr");
1352
+ const value = bytes.slice(index, index + length);
1353
+ index += length;
1354
+ if (type === 0) {
1355
+ if (identifier !== void 0) fail("Intent URI fragment repeats its identifier");
1356
+ identifier = decodeUtf8(value);
1357
+ } else if (type === 1) {
1358
+ const relay = decodeUtf8(value);
1359
+ if (!relay) fail("Intent URI fragment has an empty relay hint");
1360
+ relays.push(relay);
1361
+ } else if (type === 2) {
1362
+ if (pubkey !== void 0 || value.length !== 32) fail("Intent URI fragment has an invalid pubkey");
1363
+ pubkey = value.map((part) => part.toString(16).padStart(2, "0")).join("");
1364
+ } else if (type === 3) {
1365
+ if (kind !== void 0 || value.length !== 4) fail("Intent URI fragment has an invalid kind");
1366
+ kind = (value[0] << 24 >>> 0 | value[1] << 16 | value[2] << 8 | value[3]) >>> 0;
1367
+ } else {
1368
+ fail("Intent URI fragment has unsupported naddr data");
1369
+ }
1370
+ }
1371
+ if (!identifier || !pubkey || kind !== 35129) fail("Intent URI fragment must recommend a named 35129 napplet");
1372
+ return { address: `${kind}:${pubkey}:${identifier}`, ...relays.length > 0 ? { relays } : {} };
1373
+ };
1374
+ const normalizeHint = (value) => {
1375
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("Intent handlerHint must be an object");
1376
+ const hint = value;
1377
+ if (Object.keys(hint).some((key) => key !== "address" && key !== "relays")) fail("Intent handlerHint contains unsupported fields");
1378
+ const address = hint.address;
1379
+ if (typeof address !== "string" || !/^35129:[0-9a-f]{64}:.+$/u.test(address)) {
1380
+ fail("Intent handlerHint address must be a named 35129 coordinate");
1381
+ }
1382
+ const relays = hint.relays;
1383
+ if (relays !== void 0 && (!Array.isArray(relays) || relays.some((relay) => typeof relay !== "string" || relay.length === 0))) {
1384
+ fail("Intent handlerHint relays must be non-empty text");
1385
+ }
1386
+ return { address, ...relays === void 0 ? {} : { relays: [...relays] } };
1387
+ };
1388
+ return function normalizeIntentUri2(uri, options) {
1389
+ if (typeof uri !== "string") throw new TypeError("Intent URI must be text");
1390
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)(?:\?([^#]*))?(?:#(.*))?$/u.exec(uri);
1391
+ if (match === null) return fail("Intent URI must be napplet:<archetype>/<action>");
1392
+ const archetype = match[1];
1393
+ const action = match[2];
1394
+ if (!/^[a-z0-9][a-z0-9-]*$/u.test(archetype) || !/^[a-z0-9][a-z0-9-]*$/u.test(action)) {
1395
+ fail("Intent URI archetype and action must be lowercase slugs");
1396
+ }
1397
+ const query = match[3];
1398
+ const fragment = match[4];
1399
+ const supplied = options === void 0 ? {} : options;
1400
+ if (!supplied || typeof supplied !== "object" || Array.isArray(supplied)) fail("Intent options must be an object");
1401
+ const parsedOptions = supplied;
1402
+ if (Object.keys(parsedOptions).some((key) => key !== "payload" && key !== "handler" && key !== "handlerHint" && key !== "behavior")) {
1403
+ fail("Intent options contain unsupported fields");
1404
+ }
1405
+ const handler = parsedOptions.handler;
1406
+ if (handler !== void 0 && (typeof handler !== "string" || handler.length === 0)) {
1407
+ fail("Intent handler must be non-empty text");
1408
+ }
1409
+ let behavior;
1410
+ if (parsedOptions.behavior !== void 0) {
1411
+ if (!parsedOptions.behavior || typeof parsedOptions.behavior !== "object" || Array.isArray(parsedOptions.behavior)) fail("Intent behavior must be an object");
1412
+ const value = parsedOptions.behavior;
1413
+ if (Object.keys(value).some((key) => key !== "focus" && key !== "reuse") || hasOwn(value, "focus") && typeof value.focus !== "boolean" || hasOwn(value, "reuse") && typeof value.reuse !== "boolean") fail("Intent behavior contains unsupported fields");
1414
+ behavior = {
1415
+ ...hasOwn(value, "focus") ? { focus: value.focus } : {},
1416
+ ...hasOwn(value, "reuse") ? { reuse: value.reuse } : {}
1417
+ };
1418
+ }
1419
+ let payload;
1420
+ if (query !== void 0) {
1421
+ if (hasOwn(parsedOptions, "payload")) fail("Intent URI query cannot accompany options.payload");
1422
+ if (query.length > 0) {
1423
+ const fields = /* @__PURE__ */ Object.create(null);
1424
+ for (const field of query.split("&")) {
1425
+ const equal = field.indexOf("=");
1426
+ if (equal <= 0 || field.indexOf("=", equal + 1) >= 0) fail("Intent URI query fields must be name=value");
1427
+ const name = decode(field.slice(0, equal), "a query name");
1428
+ const value = decode(field.slice(equal + 1), "a query value");
1429
+ if (!name || hasOwn(fields, name)) fail("Intent URI query names must be unique after decoding");
1430
+ Object.defineProperty(fields, name, { value, enumerable: true, writable: true, configurable: true });
1431
+ }
1432
+ payload = fields;
1433
+ }
1434
+ } else if (hasOwn(parsedOptions, "payload")) {
1435
+ payload = parsedOptions.payload;
1436
+ }
1437
+ if (fragment !== void 0) {
1438
+ if (!fragment) fail("Intent URI fragment must be a bare naddr");
1439
+ if (parsedOptions.handlerHint !== void 0) fail("Intent URI fragment cannot accompany options.handlerHint");
1440
+ }
1441
+ const handlerHint = fragment === void 0 ? parsedOptions.handlerHint === void 0 ? void 0 : normalizeHint(parsedOptions.handlerHint) : decodeHint(fragment);
1442
+ return {
1443
+ archetype,
1444
+ action,
1445
+ convention: `napplet:${archetype}/${action}`,
1446
+ ...payload === void 0 ? {} : { payload },
1447
+ ...handlerHint === void 0 ? {} : { handlerHint },
1448
+ ...handler === void 0 ? {} : { handler },
1449
+ ...behavior === void 0 ? {} : { behavior }
1450
+ };
1451
+ };
1452
+ }
1453
+ var normalizeIntentUri = createIntentUriNormalizer();
1454
+
1249
1455
  // src/napplet-namespace.ts
1250
1456
  function uniqueBareDomains(domains) {
1251
1457
  const seen = /* @__PURE__ */ new Set();
@@ -1264,8 +1470,8 @@ function scriptJson(value) {
1264
1470
  return JSON.stringify(value).replace(/</g, "\\u003c");
1265
1471
  }
1266
1472
  function renderNappletNamespacePrelude(options) {
1267
- const domains = uniqueBareDomains(["shell", ...options.domains]);
1268
- return `<script data-kehto-nip5d-injection>(${nappletNamespacePrelude.toString()})(${scriptJson(domains)});</script>`;
1473
+ const domains = uniqueBareDomains(options.domains);
1474
+ return `<script data-kehto-nip5d-injection>(${nappletNamespacePrelude.toString()})(${scriptJson(domains)}, (${createIntentUriNormalizer.toString()})());</script>`;
1269
1475
  }
1270
1476
  function injectNappletNamespacePrelude(html, options) {
1271
1477
  const prelude = renderNappletNamespacePrelude(options);
@@ -1281,7 +1487,7 @@ function injectNappletNamespacePrelude(html, options) {
1281
1487
  }
1282
1488
  return `${prelude}${html}`;
1283
1489
  }
1284
- function nappletNamespacePrelude(domains) {
1490
+ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1285
1491
  const target = window;
1286
1492
  const allowed = new Set(domains);
1287
1493
  const requestTimeoutMs = 3e4;
@@ -1453,7 +1659,7 @@ function nappletNamespacePrelude(domains) {
1453
1659
  return Object.freeze(strings);
1454
1660
  }
1455
1661
  function supports(domain) {
1456
- return typeof domain === "string" && environment?.capabilities.domains.includes(domain) === true;
1662
+ return typeof domain === "string" && !!target.napplet && Object.prototype.hasOwnProperty.call(target.napplet, domain);
1457
1663
  }
1458
1664
  const shell2 = {
1459
1665
  ready: () => ready,
@@ -2324,7 +2530,14 @@ function nappletNamespacePrelude(domains) {
2324
2530
  }
2325
2531
  function makeIntent() {
2326
2532
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
2327
- const normalizeRequest = (value) => {
2533
+ let warnedLegacyObject = false;
2534
+ const warnLegacyObject = () => {
2535
+ if (warnedLegacyObject) return;
2536
+ warnedLegacyObject = true;
2537
+ console.warn("[KEHTO_COMPAT_INTENT_OBJECT_INVOKE] Object-form intent.invoke/open is deprecated; use intent.invoke(uri, options) or intent.open(uri, options). Warns once per napplet. See docs/compatibility.md.");
2538
+ };
2539
+ const normalizeLegacyRequest = (value) => {
2540
+ warnLegacyObject();
2328
2541
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2329
2542
  throw new TypeError("Intent request must be an object");
2330
2543
  }
@@ -2332,7 +2545,7 @@ function nappletNamespacePrelude(domains) {
2332
2545
  if (hasOwn(supplied, "sender")) {
2333
2546
  throw new TypeError("Intent callers cannot supply sender");
2334
2547
  }
2335
- const allowed2 = ["archetype", "action", "convention", "payload", "handler", "behavior"];
2548
+ const allowed2 = ["archetype", "action", "convention", "payload", "handler", "handlerHint", "behavior"];
2336
2549
  if (Object.keys(supplied).some((field) => !allowed2.includes(field))) {
2337
2550
  throw new TypeError("Intent request contains unsupported fields");
2338
2551
  }
@@ -2343,51 +2556,63 @@ function nappletNamespacePrelude(domains) {
2343
2556
  if (typeof action !== "string" || action.length === 0) {
2344
2557
  throw new TypeError("Intent action must be a non-empty string");
2345
2558
  }
2346
- if (supplied.convention !== void 0 && (typeof supplied.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(supplied.convention))) {
2347
- throw new TypeError("Intent convention must be queryless");
2559
+ const uri = supplied.convention === void 0 ? `napplet:${supplied.archetype}/${action}` : supplied.convention;
2560
+ if (typeof uri !== "string") throw new TypeError("Intent convention must be text");
2561
+ if (uri.includes("?") || uri.includes("#")) throw new TypeError("Legacy intent convention must be queryless");
2562
+ return normalizeIntentUri2(uri, {
2563
+ ...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
2564
+ ...hasOwn(supplied, "handler") ? { handler: supplied.handler } : {},
2565
+ ...hasOwn(supplied, "handlerHint") ? { handlerHint: supplied.handlerHint } : {},
2566
+ ...hasOwn(supplied, "behavior") ? { behavior: supplied.behavior } : {}
2567
+ });
2568
+ };
2569
+ const requestInvoke = (normalized) => request(
2570
+ {
2571
+ type: "intent.invoke",
2572
+ request: normalized
2573
+ },
2574
+ "intent.invoke.result",
2575
+ (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
2576
+ );
2577
+ const invoke = (uri, options) => {
2578
+ const normalized = typeof uri === "string" ? normalizeIntentUri2(uri, options) : normalizeLegacyRequest(uri);
2579
+ return requestInvoke(normalized);
2580
+ };
2581
+ const open = (uri, options, legacyOptions) => {
2582
+ if (typeof uri === "string" && uri.startsWith("napplet:") && legacyOptions === void 0) {
2583
+ const normalized2 = normalizeIntentUri2(uri, options);
2584
+ if (normalized2.action !== "open") throw new TypeError("intent.open requires a /open URI");
2585
+ return requestInvoke(normalized2);
2348
2586
  }
2349
- if (supplied.handler !== void 0 && (typeof supplied.handler !== "string" || supplied.handler.length === 0)) {
2350
- throw new TypeError("Intent handler must be a non-empty string");
2587
+ if (typeof uri !== "string" || !uri) throw new TypeError("Intent archetype must be non-empty text");
2588
+ warnLegacyObject();
2589
+ if (legacyOptions !== void 0 && (!legacyOptions || typeof legacyOptions !== "object" || Array.isArray(legacyOptions))) {
2590
+ throw new TypeError("Intent options must be an object");
2351
2591
  }
2352
- const behaviorValue = supplied?.behavior;
2353
- const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
2354
- if (behaviorValue !== void 0 && (!behavior || Array.isArray(behaviorValue) || Object.keys(behavior).some((field) => field !== "focus" && field !== "newWindow" && field !== "reuse") || hasOwn(behavior, "focus") && typeof behavior.focus !== "boolean" || hasOwn(behavior, "newWindow") && typeof behavior.newWindow !== "boolean" || hasOwn(behavior, "reuse") && typeof behavior.reuse !== "boolean")) {
2355
- throw new TypeError("Intent behavior contains unsupported fields");
2592
+ const legacy = legacyOptions;
2593
+ const convention = legacy?.convention === void 0 ? `napplet:${uri}/open` : legacy.convention;
2594
+ if (typeof convention !== "string" || convention.includes("?") || convention.includes("#")) {
2595
+ throw new TypeError("Legacy intent convention must be queryless");
2356
2596
  }
2357
- const sanitizedBehavior = {
2358
- ...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
2359
- ...typeof behavior?.newWindow === "boolean" ? { newWindow: behavior.newWindow } : {},
2360
- ...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
2361
- };
2362
- return {
2363
- archetype: supplied.archetype,
2364
- action,
2365
- ...typeof supplied.convention === "string" ? { convention: supplied.convention } : {},
2366
- ...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
2367
- ...typeof supplied.handler === "string" ? { handler: supplied.handler } : {},
2368
- ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
2369
- };
2370
- };
2371
- const invoke = (value) => {
2372
- const normalized = normalizeRequest(value);
2373
- return request(
2374
- {
2375
- type: "intent.invoke",
2376
- request: normalized
2377
- },
2378
- "intent.invoke.result",
2379
- (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
2380
- );
2597
+ const normalized = normalizeIntentUri2(`napplet:${uri}/open`, {
2598
+ ...options === void 0 ? {} : { payload: options },
2599
+ ...legacy === void 0 ? {} : Object.fromEntries(
2600
+ Object.entries(legacy).filter(([key]) => key !== "convention")
2601
+ )
2602
+ });
2603
+ const legacyNormalized = convention === `napplet:${uri}/open` ? normalized : normalizeIntentUri2(convention, {
2604
+ ...options === void 0 ? {} : { payload: options },
2605
+ ...legacy === void 0 ? {} : Object.fromEntries(
2606
+ Object.entries(legacy).filter(([key]) => key !== "convention")
2607
+ )
2608
+ });
2609
+ if (legacyNormalized.action !== "open") throw new TypeError("intent.open requires a /open URI");
2610
+ return requestInvoke(legacyNormalized);
2381
2611
  };
2382
2612
  return {
2383
2613
  onDelivery: intentDelivery.onDelivery,
2384
2614
  invoke,
2385
- open: (archetype, payload, opts) => invoke({
2386
- archetype,
2387
- action: "open",
2388
- ...payload === void 0 ? {} : { payload },
2389
- ...opts
2390
- }),
2615
+ open,
2391
2616
  available: (archetype) => request(
2392
2617
  { type: "intent.available", archetype },
2393
2618
  "intent.available.result",
@@ -2515,6 +2740,63 @@ function nappletNamespacePrelude(domains) {
2515
2740
  })
2516
2741
  };
2517
2742
  }
2743
+ function makeGamepad() {
2744
+ let state = { available: true, focused: false, pads: [] };
2745
+ const handlers = /* @__PURE__ */ new Set();
2746
+ const copy = (value) => ({
2747
+ available: value.available,
2748
+ focused: value.focused,
2749
+ ...value.reason === void 0 ? {} : { reason: value.reason },
2750
+ pads: value.pads.map((pad) => pad === null ? null : {
2751
+ index: pad.index,
2752
+ id: pad.id,
2753
+ mapping: pad.mapping,
2754
+ connected: true,
2755
+ timestamp: pad.timestamp,
2756
+ axes: [...pad.axes],
2757
+ buttons: pad.buttons.map(({ value: value2, pressed, touched }) => ({ value: value2, pressed, touched }))
2758
+ })
2759
+ });
2760
+ const numberIn = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
2761
+ const valid = (value) => {
2762
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2763
+ const candidate = value;
2764
+ if (typeof candidate.available !== "boolean" || typeof candidate.focused !== "boolean" || !Array.isArray(candidate.pads)) return false;
2765
+ if (candidate.available ? candidate.reason !== void 0 : candidate.focused || !["blocked", "unavailable"].includes(candidate.reason ?? "")) return false;
2766
+ return Array.from(candidate.pads).every((pad) => {
2767
+ if (pad === null) return true;
2768
+ if (!pad || typeof pad !== "object" || !Number.isInteger(pad.index) || pad.index < 0 || typeof pad.id !== "string" || typeof pad.mapping !== "string" || pad.connected !== true || !Number.isFinite(pad.timestamp) || !Array.isArray(pad.axes) || !Array.isArray(pad.buttons)) return false;
2769
+ if (!candidate.focused && pad.timestamp !== 0) return false;
2770
+ if (!Array.from(pad.axes).every((axis) => numberIn(axis, -1, 1) && (candidate.focused || axis === 0))) return false;
2771
+ return Array.from(pad.buttons).every((button) => button && numberIn(button.value, 0, 1) && typeof button.pressed === "boolean" && typeof button.touched === "boolean" && (candidate.focused || button.value === 0 && !button.pressed && !button.touched));
2772
+ });
2773
+ };
2774
+ listen((event) => {
2775
+ if (!isParentMessage(event)) return;
2776
+ const incoming = event.data;
2777
+ if (!incoming || incoming.type !== "gamepad.state" || "id" in incoming || !valid(incoming)) return;
2778
+ state = copy(incoming);
2779
+ const snapshot = copy(state);
2780
+ const recipients = Array.from(handlers);
2781
+ for (const entry of recipients) if (handlers.has(entry)) entry.handler(copy(snapshot));
2782
+ });
2783
+ const api = {
2784
+ get available() {
2785
+ return state.available;
2786
+ },
2787
+ get focused() {
2788
+ return state.focused;
2789
+ },
2790
+ getGamepads: () => copy(state).pads,
2791
+ onChange(handler) {
2792
+ const entry = { handler };
2793
+ handlers.add(entry);
2794
+ return subscriptionHandle(() => handlers.delete(entry));
2795
+ }
2796
+ };
2797
+ post({ type: "gamepad.subscribe" });
2798
+ return api;
2799
+ }
2518
2800
  function makeDm() {
2519
2801
  const req = (type, payload) => request({ type, ...payload }, `${type}.result`, withoutEnvelope, { rejectOnError: false });
2520
2802
  return {
@@ -2561,7 +2843,8 @@ function nappletNamespacePrelude(domains) {
2561
2843
  };
2562
2844
  }
2563
2845
  const intentDelivery = allowed.has("intent") ? makeIntentDelivery() : void 0;
2564
- const shell = makeShell();
2846
+ const shell = allowed.has("shell") ? makeShell() : void 0;
2847
+ let gamepad;
2565
2848
  let inc;
2566
2849
  let identity;
2567
2850
  let theme;
@@ -2584,9 +2867,10 @@ function nappletNamespacePrelude(domains) {
2584
2867
  return intent;
2585
2868
  }
2586
2869
  function isProtectedDomain(domain) {
2587
- return domain === "shell" || domain === "inc" || domain === "identity" || domain === "theme" || domain === "intent";
2870
+ return domain === "gamepad" || domain === "shell" || domain === "inc" || domain === "identity" || domain === "theme" || domain === "intent";
2588
2871
  }
2589
2872
  function makeDomain(domain, existing) {
2873
+ if (domain === "gamepad") return gamepad ??= makeGamepad();
2590
2874
  if (domain === "shell") return shell;
2591
2875
  if (domain === "inc") return makeProtectedInc(existing);
2592
2876
  if (domain === "identity") return makeProtectedIdentity();
@@ -2900,11 +3184,13 @@ export {
2900
3184
  createNotifyProxy,
2901
3185
  createShellBridge,
2902
3186
  createThemeProxy,
3187
+ denyNappletControllerAccess,
2903
3188
  formatDenialReason,
2904
3189
  injectNappletCsp,
2905
3190
  injectNappletNamespacePrelude,
2906
3191
  manifestCache,
2907
3192
  nappKeyRegistry,
3193
+ normalizeIntentUri,
2908
3194
  originRegistry,
2909
3195
  prepareNappletSrcdoc,
2910
3196
  renderNappletCspMeta,