@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/README.md +34 -24
- package/dist/index.d.ts +43 -3
- package/dist/index.js +341 -55
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
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
|
|
1100
|
-
if (!
|
|
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(
|
|
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" &&
|
|
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
|
-
|
|
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
|
-
|
|
2347
|
-
|
|
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 (
|
|
2350
|
-
|
|
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
|
|
2353
|
-
const
|
|
2354
|
-
if (
|
|
2355
|
-
throw new TypeError("
|
|
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
|
|
2358
|
-
...
|
|
2359
|
-
...
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
...
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
|
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,
|