@kehto/paja 0.14.0 → 0.15.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 +35 -14
- package/dist/browser-host.js +1773 -1421
- package/dist/browser-host.js.map +1 -1
- package/dist/{chunk-JHDQSR5N.js → chunk-DTRRHXYY.js} +27 -1
- package/dist/chunk-DTRRHXYY.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/dist/chunk-JHDQSR5N.js.map +0 -1
package/dist/browser-host.js
CHANGED
|
@@ -1329,7 +1329,7 @@ function handleRelayPublish(context, windowId, m) {
|
|
|
1329
1329
|
sendRelayPublishResult(hooks, windowId, id, false, void 0, "invalid event template");
|
|
1330
1330
|
return;
|
|
1331
1331
|
}
|
|
1332
|
-
const signer = hooks.auth.getSigner();
|
|
1332
|
+
const signer = hooks.auth.getSigner(windowId);
|
|
1333
1333
|
const signEvent = signer?.signEvent?.bind(signer);
|
|
1334
1334
|
if (!signEvent) {
|
|
1335
1335
|
sendRelayPublishResult(hooks, windowId, id, false, void 0, "no signer configured");
|
|
@@ -1440,7 +1440,7 @@ function handleRelayPublishEncrypted(context, windowId, msg) {
|
|
|
1440
1440
|
replyPe(false, { error: `unsupported encryption scheme: ${encryption}` });
|
|
1441
1441
|
return;
|
|
1442
1442
|
}
|
|
1443
|
-
const peSigner = hooks.auth.getSigner();
|
|
1443
|
+
const peSigner = hooks.auth.getSigner(windowId);
|
|
1444
1444
|
if (!peSigner) {
|
|
1445
1445
|
replyPe(false, { error: "no signer configured" });
|
|
1446
1446
|
return;
|
|
@@ -1449,12 +1449,9 @@ function handleRelayPublishEncrypted(context, windowId, msg) {
|
|
|
1449
1449
|
replyPe(false, { error: "invalid event template" });
|
|
1450
1450
|
return;
|
|
1451
1451
|
}
|
|
1452
|
-
publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe);
|
|
1452
|
+
publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, peSigner, replyPe);
|
|
1453
1453
|
}
|
|
1454
|
-
function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe) {
|
|
1455
|
-
const { hooks } = context;
|
|
1456
|
-
const peSigner = hooks.auth.getSigner();
|
|
1457
|
-
if (!peSigner) return;
|
|
1454
|
+
function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, peSigner, replyPe) {
|
|
1458
1455
|
(async () => {
|
|
1459
1456
|
try {
|
|
1460
1457
|
const plaintext = String(eventTemplate.content ?? "");
|
|
@@ -1697,7 +1694,7 @@ function createIdentityHandler(context) {
|
|
|
1697
1694
|
return;
|
|
1698
1695
|
}
|
|
1699
1696
|
const action = msg.type.slice("identity.".length);
|
|
1700
|
-
const signer = hooks.auth.getSigner();
|
|
1697
|
+
const signer = hooks.auth.getSigner(windowId);
|
|
1701
1698
|
const fallback = createCanonicalDomainResult(msg);
|
|
1702
1699
|
if (!fallback) return;
|
|
1703
1700
|
const sendResult = (payload = {}) => {
|
|
@@ -2450,11 +2447,8 @@ function createFirewallGate(config) {
|
|
|
2450
2447
|
if (decision === "prompt") fireConsent(windowId, napplet);
|
|
2451
2448
|
return "drop";
|
|
2452
2449
|
}
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
if (type) {
|
|
2456
|
-
hooks.sendToNapplet(windowId, { type, id, error: `firewall: ${reason}` });
|
|
2457
|
-
}
|
|
2450
|
+
const denial = createDenialResponse(envelope, `firewall: ${reason}`);
|
|
2451
|
+
if (denial) hooks.sendToNapplet(windowId, denial);
|
|
2458
2452
|
hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
|
|
2459
2453
|
if (decision === "prompt") {
|
|
2460
2454
|
fireConsent(windowId, napplet);
|
|
@@ -2476,6 +2470,19 @@ function denialResponseType(envelope) {
|
|
|
2476
2470
|
if (envelope.type.startsWith("inc.")) return null;
|
|
2477
2471
|
return `${envelope.type}.error`;
|
|
2478
2472
|
}
|
|
2473
|
+
function createDenialResponse(envelope, error) {
|
|
2474
|
+
const id = envelope.id ?? "";
|
|
2475
|
+
if (envelope.type === "outbox.query") {
|
|
2476
|
+
return {
|
|
2477
|
+
type: "outbox.query.result",
|
|
2478
|
+
id,
|
|
2479
|
+
events: [],
|
|
2480
|
+
error
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
const type = denialResponseType(envelope);
|
|
2484
|
+
return type ? { type, id, error } : null;
|
|
2485
|
+
}
|
|
2479
2486
|
function createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEnvelope, firewallGate) {
|
|
2480
2487
|
return (windowId, msg) => {
|
|
2481
2488
|
if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
|
|
@@ -2500,12 +2507,9 @@ function createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEn
|
|
|
2500
2507
|
hooks.sendToNapplet(windowId, canonicalResult);
|
|
2501
2508
|
return;
|
|
2502
2509
|
}
|
|
2503
|
-
const id = envelope.id ?? "";
|
|
2504
2510
|
const error = formatDenialReason(result2.capability);
|
|
2505
|
-
const
|
|
2506
|
-
if (
|
|
2507
|
-
hooks.sendToNapplet(windowId, { type, id, error });
|
|
2508
|
-
}
|
|
2511
|
+
const denial = createDenialResponse(envelope, error);
|
|
2512
|
+
if (denial) hooks.sendToNapplet(windowId, denial);
|
|
2509
2513
|
return;
|
|
2510
2514
|
}
|
|
2511
2515
|
}
|
|
@@ -2835,8 +2839,8 @@ function createAuthAdapter(shellHooks) {
|
|
|
2835
2839
|
getUserPubkey() {
|
|
2836
2840
|
return shellHooks.auth.getUserPubkey();
|
|
2837
2841
|
},
|
|
2838
|
-
getSigner() {
|
|
2839
|
-
return shellHooks.auth.getSigner();
|
|
2842
|
+
getSigner(windowId) {
|
|
2843
|
+
return shellHooks.auth.getSigner(windowId);
|
|
2840
2844
|
}
|
|
2841
2845
|
};
|
|
2842
2846
|
}
|
|
@@ -11087,11 +11091,20 @@ function requestKey(windowId, requestId) {
|
|
|
11087
11091
|
return `${windowId}\0${requestId}`;
|
|
11088
11092
|
}
|
|
11089
11093
|
function assertResourceOptions(options) {
|
|
11090
|
-
if (typeof options?.fetch !== "function"
|
|
11094
|
+
if (typeof options?.fetch !== "function") {
|
|
11091
11095
|
throw new Error(
|
|
11092
|
-
"[RESOURCE-01
|
|
11096
|
+
"[RESOURCE-01] createResourceService requires a runtime-provided fetch resolver."
|
|
11093
11097
|
);
|
|
11094
11098
|
}
|
|
11099
|
+
const hasOriginPolicy = options.isOriginGranted !== void 0 || options.getConnectGrants !== void 0;
|
|
11100
|
+
if (hasOriginPolicy && (typeof options.isOriginGranted !== "function" || typeof options.getConnectGrants !== "function" || typeof options.resolveIdentity !== "function")) {
|
|
11101
|
+
throw new Error(
|
|
11102
|
+
"[RESOURCE-01] optional origin policy requires {isOriginGranted, getConnectGrants, resolveIdentity} together."
|
|
11103
|
+
);
|
|
11104
|
+
}
|
|
11105
|
+
if (options.resolveIdentity !== void 0 && typeof options.resolveIdentity !== "function") {
|
|
11106
|
+
throw new Error("[RESOURCE-01] resolveIdentity must be a function when provided.");
|
|
11107
|
+
}
|
|
11095
11108
|
}
|
|
11096
11109
|
function trackRequest(state, requestId, windowId, controller) {
|
|
11097
11110
|
const key = requestKey(windowId, requestId);
|
|
@@ -11147,7 +11160,7 @@ function normalizeResourceInfo(info) {
|
|
|
11147
11160
|
}
|
|
11148
11161
|
async function resolveResourceInfo(options, windowId) {
|
|
11149
11162
|
const configured = options.resourceInfo ?? DEFAULT_RESOURCE_INFO;
|
|
11150
|
-
const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity(windowId) }) : configured;
|
|
11163
|
+
const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity?.(windowId) ?? null }) : configured;
|
|
11151
11164
|
return normalizeResourceInfo(info);
|
|
11152
11165
|
}
|
|
11153
11166
|
async function handleInfo(options, windowId, requestId, send) {
|
|
@@ -11181,22 +11194,29 @@ function classifyResourceFailure(error) {
|
|
|
11181
11194
|
message: error instanceof Error ? error.message : String(error)
|
|
11182
11195
|
};
|
|
11183
11196
|
}
|
|
11184
|
-
async function fetchResourceItem(options,
|
|
11197
|
+
async function fetchResourceItem(options, windowId, info, url, signal) {
|
|
11185
11198
|
const parsedUrl = parseResourceUrl(url);
|
|
11186
11199
|
if (!parsedUrl) return resourceInvalidRequest(url, `invalid URL: ${url}`);
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
+
if (options.isOriginGranted && options.getConnectGrants) {
|
|
11201
|
+
const identity = options.resolveIdentity?.(windowId) ?? null;
|
|
11202
|
+
if (!identity) {
|
|
11203
|
+
return {
|
|
11204
|
+
ok: false,
|
|
11205
|
+
url,
|
|
11206
|
+
error: "blocked-by-policy",
|
|
11207
|
+
message: "napplet identity not resolvable for runtime origin policy"
|
|
11208
|
+
};
|
|
11209
|
+
}
|
|
11210
|
+
const origin = parsedUrl.origin;
|
|
11211
|
+
const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
|
|
11212
|
+
if (!options.isOriginGranted(origin, grants)) {
|
|
11213
|
+
return {
|
|
11214
|
+
ok: false,
|
|
11215
|
+
url,
|
|
11216
|
+
error: "blocked-by-policy",
|
|
11217
|
+
message: `origin ${origin} not granted by runtime policy`
|
|
11218
|
+
};
|
|
11219
|
+
}
|
|
11200
11220
|
}
|
|
11201
11221
|
try {
|
|
11202
11222
|
const response = await options.fetch(url, {
|
|
@@ -11226,16 +11246,11 @@ async function fetchResourceItem(options, identity, info, url, signal) {
|
|
|
11226
11246
|
}
|
|
11227
11247
|
async function handleBytes(options, state, windowId, msg, send) {
|
|
11228
11248
|
const { requestId, url } = msg;
|
|
11229
|
-
const identity = options.resolveIdentity(windowId);
|
|
11230
|
-
if (!identity) {
|
|
11231
|
-
sendResourceError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
|
|
11232
|
-
return;
|
|
11233
|
-
}
|
|
11234
11249
|
const controller = new AbortController();
|
|
11235
11250
|
trackRequest(state, requestId, windowId, controller);
|
|
11236
11251
|
try {
|
|
11237
11252
|
const info = await resolveResourceInfo(options, windowId);
|
|
11238
|
-
const item = await fetchResourceItem(options,
|
|
11253
|
+
const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
|
|
11239
11254
|
if (controller.signal.aborted) return;
|
|
11240
11255
|
if (!item.ok) {
|
|
11241
11256
|
sendResourceError(send, requestId, item.message, item.error);
|
|
@@ -11262,11 +11277,6 @@ async function handleBytesMany(options, state, windowId, msg, send) {
|
|
|
11262
11277
|
sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty urls array", "invalid-request");
|
|
11263
11278
|
return;
|
|
11264
11279
|
}
|
|
11265
|
-
const identity = options.resolveIdentity(windowId);
|
|
11266
|
-
if (!identity) {
|
|
11267
|
-
sendBytesManyError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
|
|
11268
|
-
return;
|
|
11269
|
-
}
|
|
11270
11280
|
let info;
|
|
11271
11281
|
try {
|
|
11272
11282
|
info = await resolveResourceInfo(options, windowId);
|
|
@@ -11284,7 +11294,7 @@ async function handleBytesMany(options, state, windowId, msg, send) {
|
|
|
11284
11294
|
try {
|
|
11285
11295
|
const items = [];
|
|
11286
11296
|
for (const url of urls) {
|
|
11287
|
-
const item = await fetchResourceItem(options,
|
|
11297
|
+
const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
|
|
11288
11298
|
if (controller.signal.aborted) return;
|
|
11289
11299
|
if (item.ok) {
|
|
11290
11300
|
items.push({
|
|
@@ -11343,7 +11353,7 @@ function createResourceService(options) {
|
|
|
11343
11353
|
const descriptor = {
|
|
11344
11354
|
name: "resource",
|
|
11345
11355
|
version: RESOURCE_SERVICE_VERSION,
|
|
11346
|
-
description: "NAP-RESOURCE
|
|
11356
|
+
description: "NAP-RESOURCE service \u2014 runtime-resolved byte transport (RESOURCE-01..06)"
|
|
11347
11357
|
};
|
|
11348
11358
|
const handler = {
|
|
11349
11359
|
descriptor,
|
|
@@ -18186,6 +18196,9 @@ function createPajaResourceFetch(options = {}) {
|
|
|
18186
18196
|
if (url.protocol === "blossom:") {
|
|
18187
18197
|
return fetchBlossomResource(value, init.signal, options);
|
|
18188
18198
|
}
|
|
18199
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
18200
|
+
return fetchNetworkResource(value, init.signal, options.fetch ?? globalThis.fetch);
|
|
18201
|
+
}
|
|
18189
18202
|
if (url.protocol !== "data:") {
|
|
18190
18203
|
throw new ResourceServiceError("unsupported-scheme", `Paja does not enable ${url.protocol}`);
|
|
18191
18204
|
}
|
|
@@ -18205,7 +18218,11 @@ function createPajaResourceFetch(options = {}) {
|
|
|
18205
18218
|
};
|
|
18206
18219
|
}
|
|
18207
18220
|
function pajaResourceInfo(blossomServers = []) {
|
|
18208
|
-
const schemes = [
|
|
18221
|
+
const schemes = [
|
|
18222
|
+
{ scheme: "data", enabled: true },
|
|
18223
|
+
{ scheme: "https", enabled: true },
|
|
18224
|
+
{ scheme: "http", enabled: true }
|
|
18225
|
+
];
|
|
18209
18226
|
if (usableBlossomServers(blossomServers).length > 0) {
|
|
18210
18227
|
schemes.push({ scheme: "blossom", enabled: true });
|
|
18211
18228
|
}
|
|
@@ -18215,6 +18232,35 @@ function pajaResourceInfo(blossomServers = []) {
|
|
|
18215
18232
|
maxUrls: PAJA_RESOURCE_MAX_URLS
|
|
18216
18233
|
};
|
|
18217
18234
|
}
|
|
18235
|
+
async function fetchNetworkResource(value, signal, fetcher) {
|
|
18236
|
+
let response;
|
|
18237
|
+
try {
|
|
18238
|
+
response = await fetcher(value, {
|
|
18239
|
+
method: "GET",
|
|
18240
|
+
signal,
|
|
18241
|
+
credentials: "omit",
|
|
18242
|
+
referrerPolicy: "no-referrer"
|
|
18243
|
+
});
|
|
18244
|
+
} catch (error) {
|
|
18245
|
+
if (signal.aborted || isAbortError(error)) throw error;
|
|
18246
|
+
throw new ResourceServiceError(
|
|
18247
|
+
"network-error",
|
|
18248
|
+
error instanceof Error ? error.message : String(error)
|
|
18249
|
+
);
|
|
18250
|
+
}
|
|
18251
|
+
if (response.status === 404 || response.status === 410) {
|
|
18252
|
+
throw new ResourceServiceError("not-found", "HTTP resource was not found");
|
|
18253
|
+
}
|
|
18254
|
+
if (!response.ok) {
|
|
18255
|
+
throw new ResourceServiceError("network-error", `HTTP resource returned ${response.status}`);
|
|
18256
|
+
}
|
|
18257
|
+
const bytes = await readCappedResponse(response, signal);
|
|
18258
|
+
const mime = sniffSafeResourceMime(bytes);
|
|
18259
|
+
if (!mime) {
|
|
18260
|
+
throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
|
|
18261
|
+
}
|
|
18262
|
+
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
18263
|
+
}
|
|
18218
18264
|
async function fetchBlossomResource(value, signal, options) {
|
|
18219
18265
|
const match = BLOSSOM_RESOURCE_PATTERN.exec(value);
|
|
18220
18266
|
if (!match?.[1]) {
|
|
@@ -19421,12 +19467,17 @@ async function createPajaBrowserFsBackend(options) {
|
|
|
19421
19467
|
// src/browser-dev-runtime.ts
|
|
19422
19468
|
var DEV_SIGNER_SECRET_KEY = generateSecretKey();
|
|
19423
19469
|
var PAJA_DEV_SIGNER_PUBKEY = getPublicKey(DEV_SIGNER_SECRET_KEY);
|
|
19424
|
-
function createPajaDevSigner(getSimulation, confirm) {
|
|
19470
|
+
function createPajaDevSigner(getSimulation, confirm, source) {
|
|
19425
19471
|
return {
|
|
19426
19472
|
getPublicKey: () => PAJA_DEV_SIGNER_PUBKEY,
|
|
19427
19473
|
getRelays: () => Object.fromEntries(getPajaRelayUrls(getSimulation()).map((relay) => [relay, { read: true, write: true }])),
|
|
19428
19474
|
async signEvent(event) {
|
|
19429
|
-
|
|
19475
|
+
const signerContext = source ? { ...source, signerPubkey: PAJA_DEV_SIGNER_PUBKEY } : void 0;
|
|
19476
|
+
if (!await confirm({
|
|
19477
|
+
action: "sign",
|
|
19478
|
+
event,
|
|
19479
|
+
...signerContext ? { signerContext } : {}
|
|
19480
|
+
})) {
|
|
19430
19481
|
throw new Error("Paja signing request denied");
|
|
19431
19482
|
}
|
|
19432
19483
|
const template = { ...event };
|
|
@@ -19549,10 +19600,12 @@ function createDevTheme(mode, values) {
|
|
|
19549
19600
|
function getRuntimePubkey(getSimulation, signerProvider) {
|
|
19550
19601
|
return signerProvider?.getPubkey() || (signerProvider?.getMethod() === "dev" ? PAJA_DEV_SIGNER_PUBKEY : "") || getSimulation().identity.pubkey;
|
|
19551
19602
|
}
|
|
19552
|
-
function createRuntimeSigner(getSimulation, confirmRequest, signerProvider) {
|
|
19553
|
-
const signer = signerProvider?.getSigner();
|
|
19603
|
+
function createRuntimeSigner(getSimulation, confirmRequest, signerProvider, source) {
|
|
19604
|
+
const signer = signerProvider?.getSigner(source);
|
|
19554
19605
|
if (!signer) {
|
|
19555
|
-
if (signerProvider?.getMethod() === "dev")
|
|
19606
|
+
if (signerProvider?.getMethod() === "dev") {
|
|
19607
|
+
return createPajaDevSigner(getSimulation, confirmRequest, source);
|
|
19608
|
+
}
|
|
19556
19609
|
const fixedPubkey = getSimulation().identity.pubkey;
|
|
19557
19610
|
if (fixedPubkey) {
|
|
19558
19611
|
return {
|
|
@@ -19596,9 +19649,6 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
|
|
|
19596
19649
|
const services = {
|
|
19597
19650
|
resource: createResourceService({
|
|
19598
19651
|
fetch: createPajaResourceFetch({ getBlossomServers }),
|
|
19599
|
-
isOriginGranted: (origin, grants) => grants.includes(origin),
|
|
19600
|
-
getConnectGrants: () => ["null"],
|
|
19601
|
-
resolveIdentity: (windowId) => getIdentity?.(windowId) ?? null,
|
|
19602
19652
|
resourceInfo: () => pajaResourceInfo(getBlossomServers())
|
|
19603
19653
|
})
|
|
19604
19654
|
};
|
|
@@ -19772,6 +19822,18 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
|
|
|
19772
19822
|
return { services, refreshAvailability };
|
|
19773
19823
|
}
|
|
19774
19824
|
function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadcast, confirmRequest, signerProvider, getIdentity, onEnvironmentChanged, intentHost, userActivation, notifyOptions, configOptions) {
|
|
19825
|
+
const resolveIdentity = (windowId) => getIdentity?.(windowId) ?? {
|
|
19826
|
+
dTag: config.window.dTag,
|
|
19827
|
+
aggregateHash: config.window.aggregateHash
|
|
19828
|
+
};
|
|
19829
|
+
const signerSource = (windowId) => {
|
|
19830
|
+
const napplet = resolveIdentity(windowId);
|
|
19831
|
+
return {
|
|
19832
|
+
windowId,
|
|
19833
|
+
napplet,
|
|
19834
|
+
runtimeScope: config.target?.mode === "iframe-url" ? `target-url:${config.target.url}` : `artifact:${napplet.aggregateHash}`
|
|
19835
|
+
};
|
|
19836
|
+
};
|
|
19775
19837
|
const relayBackend = createPajaRelayBackend(getSimulation, confirmRequest);
|
|
19776
19838
|
const relayConfig = createPajaRelayConfig(getSimulation);
|
|
19777
19839
|
const uploadRuntime = getSimulation().upload.mode === "blossom" ? createPajaUploadRuntime({
|
|
@@ -19781,10 +19843,7 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
|
|
|
19781
19843
|
queryDiscovery: (relayUrls2, filters) => relayBackend.query(relayUrls2, filters),
|
|
19782
19844
|
getRelayUrls: () => relayConfig.getRelayUrls(["discovery", "super"]),
|
|
19783
19845
|
confirmRequest,
|
|
19784
|
-
getNappletIdentity:
|
|
19785
|
-
dTag: config.window.dTag,
|
|
19786
|
-
aggregateHash: config.window.aggregateHash
|
|
19787
|
-
},
|
|
19846
|
+
getNappletIdentity: resolveIdentity,
|
|
19788
19847
|
subscribeSignerChange: signerProvider?.subscribe?.bind(signerProvider)
|
|
19789
19848
|
}) : void 0;
|
|
19790
19849
|
const getBlossomServers = () => [
|
|
@@ -19804,20 +19863,14 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
|
|
|
19804
19863
|
uploadRuntime,
|
|
19805
19864
|
signerProvider,
|
|
19806
19865
|
intentHost,
|
|
19807
|
-
|
|
19808
|
-
dTag: config.window.dTag,
|
|
19809
|
-
aggregateHash: config.window.aggregateHash
|
|
19810
|
-
})),
|
|
19866
|
+
resolveIdentity,
|
|
19811
19867
|
userActivation,
|
|
19812
19868
|
notifyOptions,
|
|
19813
19869
|
configOptions
|
|
19814
19870
|
);
|
|
19815
19871
|
const services = serviceBundle.services;
|
|
19816
19872
|
const ready = createPajaBrowserFsBackend({
|
|
19817
|
-
getIdentity:
|
|
19818
|
-
dTag: config.window.dTag,
|
|
19819
|
-
aggregateHash: config.window.aggregateHash
|
|
19820
|
-
},
|
|
19873
|
+
getIdentity: resolveIdentity,
|
|
19821
19874
|
userActivation
|
|
19822
19875
|
}).then((fsBackend) => {
|
|
19823
19876
|
if (!fsBackend) return;
|
|
@@ -19839,7 +19892,12 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
|
|
|
19839
19892
|
windowManager: { createWindow: () => null },
|
|
19840
19893
|
auth: {
|
|
19841
19894
|
getUserPubkey: () => getRuntimePubkey(getSimulation, signerProvider),
|
|
19842
|
-
getSigner: () => createRuntimeSigner(
|
|
19895
|
+
getSigner: (windowId) => createRuntimeSigner(
|
|
19896
|
+
getSimulation,
|
|
19897
|
+
confirmRequest,
|
|
19898
|
+
signerProvider,
|
|
19899
|
+
windowId ? signerSource(windowId) : void 0
|
|
19900
|
+
)
|
|
19843
19901
|
},
|
|
19844
19902
|
services,
|
|
19845
19903
|
get capabilities() {
|
|
@@ -19884,621 +19942,1214 @@ function forwardPajaHotkey(event) {
|
|
|
19884
19942
|
}));
|
|
19885
19943
|
}
|
|
19886
19944
|
|
|
19887
|
-
// src/
|
|
19888
|
-
var
|
|
19889
|
-
|
|
19890
|
-
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
|
|
19894
|
-
|
|
19895
|
-
|
|
19896
|
-
|
|
19897
|
-
|
|
19898
|
-
|
|
19899
|
-
|
|
19900
|
-
|
|
19901
|
-
|
|
19902
|
-
|
|
19903
|
-
|
|
19904
|
-
|
|
19945
|
+
// src/runtime-resolver.ts
|
|
19946
|
+
var PAJA_NAPPLET_MANIFEST_KINDS = NAPPLET_KINDS;
|
|
19947
|
+
function decodePajaPointer(value) {
|
|
19948
|
+
const trimmed = value.trim();
|
|
19949
|
+
const decoded = decode(trimmed);
|
|
19950
|
+
if (decoded.type === "naddr") {
|
|
19951
|
+
return {
|
|
19952
|
+
type: "naddr",
|
|
19953
|
+
value: trimmed,
|
|
19954
|
+
identifier: decoded.data.identifier,
|
|
19955
|
+
pubkey: decoded.data.pubkey,
|
|
19956
|
+
kind: decoded.data.kind,
|
|
19957
|
+
relays: [...decoded.data.relays ?? []]
|
|
19958
|
+
};
|
|
19959
|
+
}
|
|
19960
|
+
if (decoded.type === "nevent") {
|
|
19961
|
+
return {
|
|
19962
|
+
type: "nevent",
|
|
19963
|
+
value: trimmed,
|
|
19964
|
+
id: decoded.data.id,
|
|
19965
|
+
...decoded.data.author ? { author: decoded.data.author } : {},
|
|
19966
|
+
...decoded.data.kind ? { kind: decoded.data.kind } : {},
|
|
19967
|
+
relays: [...decoded.data.relays ?? []]
|
|
19968
|
+
};
|
|
19905
19969
|
}
|
|
19970
|
+
throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
|
|
19906
19971
|
}
|
|
19907
|
-
function
|
|
19908
|
-
const
|
|
19909
|
-
|
|
19910
|
-
|
|
19972
|
+
async function resolvePajaPointer(value, options = {}) {
|
|
19973
|
+
const pointer = decodePajaPointer(value);
|
|
19974
|
+
const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
|
|
19975
|
+
if (relays.length === 0) {
|
|
19976
|
+
throw new Error("Pointer has no relay hints or enabled configured live relays.");
|
|
19911
19977
|
}
|
|
19912
|
-
|
|
19978
|
+
const pool = options.pool ?? new SimplePool();
|
|
19979
|
+
const ownsPool = options.pool === void 0;
|
|
19913
19980
|
try {
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19981
|
+
const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
|
|
19982
|
+
const blossomServers = [...options.blossomServers ?? []];
|
|
19983
|
+
const resolved = await resolveNapplet({
|
|
19984
|
+
event,
|
|
19985
|
+
cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
|
|
19986
|
+
fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
|
|
19987
|
+
const response = await (options.fetcher ?? fetch)(url);
|
|
19988
|
+
if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
|
|
19989
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
19990
|
+
})
|
|
19991
|
+
});
|
|
19992
|
+
return {
|
|
19993
|
+
pointer,
|
|
19994
|
+
event,
|
|
19995
|
+
relays,
|
|
19996
|
+
blossomServers,
|
|
19997
|
+
dTag: resolved.dTag,
|
|
19998
|
+
aggregateHash: resolved.aggregateHash,
|
|
19999
|
+
indexHtml: resolved.indexHtml,
|
|
20000
|
+
manifest: resolved.manifest
|
|
20001
|
+
};
|
|
20002
|
+
} finally {
|
|
20003
|
+
if (ownsPool) {
|
|
20004
|
+
pool.close?.(relays);
|
|
20005
|
+
pool.destroy?.();
|
|
20006
|
+
}
|
|
19917
20007
|
}
|
|
19918
20008
|
}
|
|
19919
|
-
function
|
|
19920
|
-
|
|
19921
|
-
const
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
20009
|
+
function injectPajaRuntimeCsp(html, origins) {
|
|
20010
|
+
const grantedOrigins = [...new Set(origins)].sort();
|
|
20011
|
+
const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
|
|
20012
|
+
const value = [
|
|
20013
|
+
"default-src 'none'",
|
|
20014
|
+
"script-src 'unsafe-inline'",
|
|
20015
|
+
"style-src 'unsafe-inline'",
|
|
20016
|
+
"img-src data: blob:",
|
|
20017
|
+
"font-src data:",
|
|
20018
|
+
connectSrc,
|
|
20019
|
+
"worker-src 'none'",
|
|
20020
|
+
"child-src 'none'",
|
|
20021
|
+
"frame-src 'none'",
|
|
20022
|
+
"media-src 'none'",
|
|
20023
|
+
"object-src 'none'",
|
|
20024
|
+
"manifest-src 'none'",
|
|
20025
|
+
"prefetch-src 'none'",
|
|
20026
|
+
"base-uri 'none'",
|
|
20027
|
+
"form-action 'none'",
|
|
20028
|
+
"frame-ancestors 'self'"
|
|
20029
|
+
].join("; ");
|
|
20030
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(value)}">`;
|
|
20031
|
+
if (/<head[^>]*>/i.test(html)) {
|
|
20032
|
+
return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
|
|
19925
20033
|
}
|
|
19926
|
-
|
|
19927
|
-
}
|
|
19928
|
-
function domainEnabled(simulation, domain) {
|
|
19929
|
-
return simulation.capabilities.domains[domain] === true;
|
|
19930
|
-
}
|
|
19931
|
-
function getTargetIdentity(config) {
|
|
19932
|
-
return {
|
|
19933
|
-
pubkey: "",
|
|
19934
|
-
dTag: config.window.dTag,
|
|
19935
|
-
aggregateHash: config.window.aggregateHash
|
|
19936
|
-
};
|
|
19937
|
-
}
|
|
19938
|
-
function appendPajaMessageLog(state, direction, raw, windowId) {
|
|
19939
|
-
if (!state) return;
|
|
19940
|
-
state.messageLog.push({
|
|
19941
|
-
index: state.messageLog.length,
|
|
19942
|
-
timestamp: Date.now(),
|
|
19943
|
-
direction,
|
|
19944
|
-
type: describeMessage(raw),
|
|
19945
|
-
windowId,
|
|
19946
|
-
preview: previewMessage(raw),
|
|
19947
|
-
detail: describeMessageDetail(raw)
|
|
19948
|
-
});
|
|
19949
|
-
if (state.messageLog.length > PAJA_LOG_LIMIT) {
|
|
19950
|
-
state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
|
|
20034
|
+
if (/<html[^>]*>/i.test(html)) {
|
|
20035
|
+
return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
|
|
19951
20036
|
}
|
|
19952
|
-
|
|
20037
|
+
return `${meta}${html}`;
|
|
19953
20038
|
}
|
|
19954
|
-
function
|
|
19955
|
-
const
|
|
19956
|
-
|
|
19957
|
-
|
|
19958
|
-
|
|
19959
|
-
|
|
19960
|
-
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
return (msg, targetOrigin, transfer) => {
|
|
19964
|
-
const context = proxyContexts.get(proxy);
|
|
19965
|
-
appendPajaMessageLog(
|
|
19966
|
-
context?.state ?? state,
|
|
19967
|
-
"shell->napplet",
|
|
19968
|
-
redactPajaMessageForLog(msg),
|
|
19969
|
-
context?.windowId ?? windowId
|
|
19970
|
-
);
|
|
19971
|
-
return target.postMessage(msg, targetOrigin, transfer);
|
|
19972
|
-
};
|
|
19973
|
-
}
|
|
19974
|
-
try {
|
|
19975
|
-
const val = Reflect.get(target, prop, target);
|
|
19976
|
-
return typeof val === "function" ? val.bind(target) : val;
|
|
19977
|
-
} catch {
|
|
19978
|
-
return void 0;
|
|
19979
|
-
}
|
|
20039
|
+
async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
|
|
20040
|
+
const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
|
|
20041
|
+
const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
|
|
20042
|
+
const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
|
|
20043
|
+
if (!event) {
|
|
20044
|
+
if (result2.timedOut) {
|
|
20045
|
+
throw new Error(
|
|
20046
|
+
`Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
|
|
20047
|
+
);
|
|
19980
20048
|
}
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
20049
|
+
const failures = relayFailureReasons(result2.reasons);
|
|
20050
|
+
if (result2.error || failures.length > 0) {
|
|
20051
|
+
const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
|
|
20052
|
+
throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
|
|
20053
|
+
}
|
|
20054
|
+
throw new Error(
|
|
20055
|
+
`All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
|
|
20056
|
+
);
|
|
20057
|
+
}
|
|
20058
|
+
if (!isNappletManifestKind(event.kind)) {
|
|
20059
|
+
throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
|
|
20060
|
+
}
|
|
20061
|
+
return event;
|
|
19986
20062
|
}
|
|
19987
|
-
function
|
|
19988
|
-
if (
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
values: "[redacted by host]"
|
|
19993
|
-
};
|
|
19994
|
-
}
|
|
19995
|
-
function isRecord6(value) {
|
|
19996
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19997
|
-
}
|
|
19998
|
-
function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
|
|
19999
|
-
const originalGetIframeWindow = originRegistry2.getIframeWindow.bind(originRegistry2);
|
|
20000
|
-
originRegistry2.getIframeWindow = (windowId) => {
|
|
20001
|
-
const win = originalGetIframeWindow(windowId);
|
|
20002
|
-
if (!win) return null;
|
|
20003
|
-
return createPajaPostMessageProxy(win, stateRef(), windowId);
|
|
20004
|
-
};
|
|
20005
|
-
const originalGetWindowId = originRegistry2.getWindowId.bind(originRegistry2);
|
|
20006
|
-
originRegistry2.getWindowId = (win) => {
|
|
20007
|
-
const result2 = originalGetWindowId(win);
|
|
20008
|
-
if (result2) return result2;
|
|
20009
|
-
const real = proxyToReal.get(win);
|
|
20010
|
-
return real ? originalGetWindowId(real) : void 0;
|
|
20011
|
-
};
|
|
20012
|
-
const originalGetIdentity = originRegistry2.getIdentity.bind(originRegistry2);
|
|
20013
|
-
originRegistry2.getIdentity = (win) => {
|
|
20014
|
-
const real = proxyToReal.get(win);
|
|
20015
|
-
return originalGetIdentity(win) ?? (real ? originalGetIdentity(real) : void 0);
|
|
20016
|
-
};
|
|
20017
|
-
const originalGetRegistrationId = originRegistry2.getRegistrationId.bind(originRegistry2);
|
|
20018
|
-
originRegistry2.getRegistrationId = (win) => {
|
|
20019
|
-
const real = proxyToReal.get(win);
|
|
20020
|
-
return originalGetRegistrationId(win) ?? (real ? originalGetRegistrationId(real) : void 0);
|
|
20021
|
-
};
|
|
20022
|
-
const originalGetEnvironment = originRegistry2.getEnvironment.bind(originRegistry2);
|
|
20023
|
-
originRegistry2.getEnvironment = (win) => {
|
|
20024
|
-
const real = proxyToReal.get(win);
|
|
20025
|
-
return originalGetEnvironment(win) ?? (real ? originalGetEnvironment(real) : void 0);
|
|
20026
|
-
};
|
|
20027
|
-
}
|
|
20028
|
-
function renderPajaDevtools(state, options) {
|
|
20029
|
-
renderInterfaceControls(state);
|
|
20030
|
-
renderAclControls(state, options.bridge);
|
|
20031
|
-
renderSignerStatus(state, options.devSignerPubkey);
|
|
20063
|
+
function queryPointerRelays(pool, relays, filter, maxWaitMs) {
|
|
20064
|
+
if (pool.subscribeManyEose) {
|
|
20065
|
+
return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
|
|
20066
|
+
}
|
|
20067
|
+
return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
|
|
20032
20068
|
}
|
|
20033
|
-
function
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
const
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20069
|
+
function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
|
|
20070
|
+
return new Promise((resolve) => {
|
|
20071
|
+
const events = [];
|
|
20072
|
+
let settled = false;
|
|
20073
|
+
let subscription;
|
|
20074
|
+
const finish = (result2) => {
|
|
20075
|
+
if (settled) return;
|
|
20076
|
+
settled = true;
|
|
20077
|
+
clearTimeout(deadline);
|
|
20078
|
+
resolve({ events, ...result2 });
|
|
20079
|
+
};
|
|
20080
|
+
const deadline = setTimeout(() => {
|
|
20081
|
+
finish({ reasons: [], timedOut: true });
|
|
20082
|
+
void subscription?.close("Paja pointer relay deadline exceeded");
|
|
20083
|
+
}, maxWaitMs);
|
|
20084
|
+
try {
|
|
20085
|
+
subscription = pool.subscribeManyEose([...relays], filter, {
|
|
20086
|
+
maxWait: maxWaitMs,
|
|
20087
|
+
label: "kehto-paja-runtime",
|
|
20088
|
+
onevent(event) {
|
|
20089
|
+
events.push(event);
|
|
20090
|
+
},
|
|
20091
|
+
onclose(reasons) {
|
|
20092
|
+
finish({ reasons, timedOut: false });
|
|
20093
|
+
}
|
|
20094
|
+
});
|
|
20095
|
+
} catch (error) {
|
|
20096
|
+
finish({ reasons: [], timedOut: false, error });
|
|
20058
20097
|
}
|
|
20059
|
-
row.append(dir, body);
|
|
20060
|
-
return row;
|
|
20061
20098
|
});
|
|
20062
|
-
container.replaceChildren(...rows);
|
|
20063
|
-
container.scrollTop = container.scrollHeight;
|
|
20064
20099
|
}
|
|
20065
|
-
function
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
20074
|
-
|
|
20075
|
-
|
|
20076
|
-
|
|
20077
|
-
|
|
20078
|
-
|
|
20079
|
-
|
|
20080
|
-
|
|
20081
|
-
|
|
20100
|
+
function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
|
|
20101
|
+
return new Promise((resolve) => {
|
|
20102
|
+
let settled = false;
|
|
20103
|
+
const finish = (result2) => {
|
|
20104
|
+
if (settled) return;
|
|
20105
|
+
settled = true;
|
|
20106
|
+
clearTimeout(deadline);
|
|
20107
|
+
resolve(result2);
|
|
20108
|
+
};
|
|
20109
|
+
const deadline = setTimeout(() => {
|
|
20110
|
+
finish({ events: [], reasons: [], timedOut: true });
|
|
20111
|
+
}, maxWaitMs);
|
|
20112
|
+
void pool.querySync([...relays], filter, {
|
|
20113
|
+
maxWait: maxWaitMs,
|
|
20114
|
+
label: "kehto-paja-runtime"
|
|
20115
|
+
}).then(
|
|
20116
|
+
(events) => finish({ events, reasons: [], timedOut: false }),
|
|
20117
|
+
(error) => finish({ events: [], reasons: [], timedOut: false, error })
|
|
20118
|
+
);
|
|
20119
|
+
});
|
|
20082
20120
|
}
|
|
20083
|
-
function
|
|
20084
|
-
|
|
20085
|
-
if (!container) return;
|
|
20086
|
-
const identity = getTargetIdentity(state.config);
|
|
20087
|
-
container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
|
|
20088
|
-
const allowed2 = bridge?.runtime.aclState.check(
|
|
20089
|
-
identity.pubkey,
|
|
20090
|
-
identity.dTag,
|
|
20091
|
-
identity.aggregateHash,
|
|
20092
|
-
capability
|
|
20093
|
-
) ?? true;
|
|
20094
|
-
const button2 = document.createElement("button");
|
|
20095
|
-
button2.type = "button";
|
|
20096
|
-
button2.className = "toggle";
|
|
20097
|
-
button2.dataset.aclCapability = capability;
|
|
20098
|
-
button2.dataset.enabled = String(allowed2);
|
|
20099
|
-
button2.textContent = capability;
|
|
20100
|
-
button2.title = `${capability} ${allowed2 ? "allowed" : "blocked"}`;
|
|
20101
|
-
button2.addEventListener("click", () => state.setAclCapability(capability, !allowed2));
|
|
20102
|
-
return button2;
|
|
20103
|
-
}));
|
|
20121
|
+
function relayFailureReasons(reasons) {
|
|
20122
|
+
return [...new Set(reasons.map((reason) => reason.trim()).filter((reason) => reason.length > 0 && reason !== "closed automatically on eose").map((reason) => reason.slice(0, 160)))].slice(0, 3);
|
|
20104
20123
|
}
|
|
20105
|
-
function
|
|
20106
|
-
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
|
|
20110
|
-
|
|
20111
|
-
const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
|
|
20112
|
-
const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
|
|
20113
|
-
const prefix = state.signer.status === "disconnected" ? "no signer connected" : state.signer.status === "error" ? `${methodLabel} error: ${state.signer.error ?? "unknown error"} \xB7 fallback pubkey ${fallbackPubkey}` : `${methodLabel} ${state.signer.status} \xB7 pubkey ${pubkey2 || "(none)"}${relay}`;
|
|
20114
|
-
el.textContent = `${prefix} \xB7 every sign/publish request prompts`;
|
|
20115
|
-
const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
|
|
20116
|
-
const devButton = document.createElement("button");
|
|
20117
|
-
devButton.type = "button";
|
|
20118
|
-
devButton.id = "signer-dev";
|
|
20119
|
-
devButton.dataset.active = String(state.signer.method === "dev");
|
|
20120
|
-
devButton.textContent = "Dev";
|
|
20121
|
-
devButton.addEventListener("click", () => state.useDevSigner());
|
|
20122
|
-
const nip07Button = document.createElement("button");
|
|
20123
|
-
nip07Button.type = "button";
|
|
20124
|
-
nip07Button.id = "signer-nip07";
|
|
20125
|
-
nip07Button.dataset.active = String(state.signer.method === "nip07");
|
|
20126
|
-
nip07Button.textContent = "NIP-07";
|
|
20127
|
-
nip07Button.addEventListener("click", () => void state.connectNip07());
|
|
20128
|
-
const bunkerInput = document.createElement("input");
|
|
20129
|
-
bunkerInput.id = "signer-bunker-uri";
|
|
20130
|
-
bunkerInput.type = "text";
|
|
20131
|
-
bunkerInput.autocomplete = "off";
|
|
20132
|
-
bunkerInput.placeholder = "bunker://...";
|
|
20133
|
-
bunkerInput.setAttribute("aria-label", "Bunker connection URI");
|
|
20134
|
-
bunkerInput.value = previousInput;
|
|
20135
|
-
const bunkerButton = document.createElement("button");
|
|
20136
|
-
bunkerButton.type = "button";
|
|
20137
|
-
bunkerButton.id = "signer-bunker";
|
|
20138
|
-
bunkerButton.dataset.active = String(state.signer.method === "nip46");
|
|
20139
|
-
bunkerButton.textContent = "Bunker";
|
|
20140
|
-
bunkerButton.addEventListener("click", () => void state.connectBunker(bunkerInput.value));
|
|
20141
|
-
controls.replaceChildren(devButton, nip07Button, bunkerInput, bunkerButton);
|
|
20124
|
+
function normalizeMaxWaitMs(value) {
|
|
20125
|
+
if (value === void 0) return 5e3;
|
|
20126
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
20127
|
+
throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
|
|
20128
|
+
}
|
|
20129
|
+
return Math.floor(value);
|
|
20142
20130
|
}
|
|
20143
|
-
|
|
20144
|
-
|
|
20145
|
-
|
|
20146
|
-
if (!uri || typeof uri !== "string") return null;
|
|
20147
|
-
let rest;
|
|
20148
|
-
if (uri.startsWith("bunker://")) {
|
|
20149
|
-
rest = uri.slice("bunker://".length);
|
|
20150
|
-
} else if (uri.startsWith("nostrconnect://")) {
|
|
20151
|
-
rest = uri.slice("nostrconnect://".length);
|
|
20152
|
-
} else {
|
|
20153
|
-
return null;
|
|
20131
|
+
function addressableFilter(pointer) {
|
|
20132
|
+
if (pointer.kind !== NAPPLET_KIND_NAMED) {
|
|
20133
|
+
throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
|
|
20154
20134
|
}
|
|
20155
|
-
|
|
20156
|
-
|
|
20157
|
-
|
|
20158
|
-
|
|
20159
|
-
|
|
20160
|
-
|
|
20161
|
-
if (!relay) return null;
|
|
20162
|
-
return { pubkey: pubkey2, relay, secret: params.get("secret") ?? void 0 };
|
|
20135
|
+
return {
|
|
20136
|
+
kinds: [NAPPLET_KIND_NAMED],
|
|
20137
|
+
authors: [pointer.pubkey],
|
|
20138
|
+
"#d": [pointer.identifier],
|
|
20139
|
+
limit: 1
|
|
20140
|
+
};
|
|
20163
20141
|
}
|
|
20164
|
-
function
|
|
20165
|
-
const localSecretKey = normalizeLocalSecretKey(options.localSecretKey ?? generateSecretKey());
|
|
20166
|
-
const localPubkey = getPublicKey(localSecretKey);
|
|
20142
|
+
function eventFilter(pointer) {
|
|
20167
20143
|
return {
|
|
20168
|
-
|
|
20169
|
-
|
|
20170
|
-
|
|
20171
|
-
|
|
20172
|
-
localPubkey,
|
|
20173
|
-
pending: /* @__PURE__ */ new Map(),
|
|
20174
|
-
subId: `paja-nip46-${localPubkey.substring(0, 8)}`,
|
|
20175
|
-
ws: null,
|
|
20176
|
-
authorizedPubkey: null
|
|
20144
|
+
ids: [pointer.id],
|
|
20145
|
+
...pointer.kind ? { kinds: [pointer.kind] } : {},
|
|
20146
|
+
...pointer.author ? { authors: [pointer.author] } : {},
|
|
20147
|
+
limit: 1
|
|
20177
20148
|
};
|
|
20178
20149
|
}
|
|
20179
|
-
function
|
|
20180
|
-
if (
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
|
|
20184
|
-
|
|
20185
|
-
function cleanupNip46State(state) {
|
|
20186
|
-
for (const entry of state.pending.values()) {
|
|
20187
|
-
clearTimeout(entry.timer);
|
|
20188
|
-
entry.reject(new Error("NIP-46 client closed"));
|
|
20189
|
-
}
|
|
20190
|
-
state.pending.clear();
|
|
20191
|
-
if (state.ws && state.ws.readyState !== WebSocket.CLOSED) {
|
|
20192
|
-
state.ws.close();
|
|
20150
|
+
function matchesPointer(pointer, event) {
|
|
20151
|
+
if (pointer.type === "nevent") {
|
|
20152
|
+
if (event.id !== pointer.id) return false;
|
|
20153
|
+
if (pointer.author && event.pubkey !== pointer.author) return false;
|
|
20154
|
+
if (pointer.kind && event.kind !== pointer.kind) return false;
|
|
20155
|
+
return true;
|
|
20193
20156
|
}
|
|
20194
|
-
|
|
20157
|
+
return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
|
|
20195
20158
|
}
|
|
20196
|
-
|
|
20197
|
-
|
|
20198
|
-
|
|
20159
|
+
function uniqueRelayUrls(values) {
|
|
20160
|
+
const out = [];
|
|
20161
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20162
|
+
for (const value of values) {
|
|
20163
|
+
const url = normalizeRelayUrl2(value);
|
|
20164
|
+
if (!url || seen.has(url)) continue;
|
|
20165
|
+
seen.add(url);
|
|
20166
|
+
out.push(url);
|
|
20199
20167
|
}
|
|
20200
|
-
|
|
20201
|
-
const encryptedContent = encrypt(payload, getConversationKey(state.localSecretKey, state.bunkerPubkey));
|
|
20202
|
-
const signedEvent = finalizeEvent({
|
|
20203
|
-
kind: 24133,
|
|
20204
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
20205
|
-
tags: [["p", state.bunkerPubkey]],
|
|
20206
|
-
content: encryptedContent
|
|
20207
|
-
}, state.localSecretKey);
|
|
20208
|
-
return new Promise((resolve, reject2) => {
|
|
20209
|
-
const timer = setTimeout(() => {
|
|
20210
|
-
state.pending.delete(correlationId);
|
|
20211
|
-
reject2(new Error(`NIP-46 request timed out: ${method}`));
|
|
20212
|
-
}, state.timeout);
|
|
20213
|
-
state.pending.set(correlationId, { resolve, reject: reject2, timer });
|
|
20214
|
-
state.ws.send(JSON.stringify(["EVENT", signedEvent]));
|
|
20215
|
-
});
|
|
20168
|
+
return out;
|
|
20216
20169
|
}
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
msg = JSON.parse(data);
|
|
20221
|
-
} catch {
|
|
20222
|
-
return;
|
|
20223
|
-
}
|
|
20224
|
-
if (!Array.isArray(msg) || msg[0] !== "EVENT") return;
|
|
20225
|
-
const event = msg[2];
|
|
20226
|
-
if (!event || event.kind !== 24133 || typeof event.content !== "string") return;
|
|
20227
|
-
let response;
|
|
20170
|
+
function normalizeRelayUrl2(value) {
|
|
20171
|
+
const raw = value.trim();
|
|
20172
|
+
if (!raw) return void 0;
|
|
20228
20173
|
try {
|
|
20229
|
-
|
|
20174
|
+
const url = new URL(raw);
|
|
20175
|
+
if (url.protocol !== "wss:" && url.protocol !== "ws:") return void 0;
|
|
20176
|
+
url.hash = "";
|
|
20177
|
+
url.search = "";
|
|
20178
|
+
return url.href.replace(/\/$/, "");
|
|
20230
20179
|
} catch {
|
|
20231
|
-
return;
|
|
20232
|
-
}
|
|
20233
|
-
const entry = state.pending.get(response.id);
|
|
20234
|
-
if (!entry) return;
|
|
20235
|
-
clearTimeout(entry.timer);
|
|
20236
|
-
state.pending.delete(response.id);
|
|
20237
|
-
if (response.error) {
|
|
20238
|
-
entry.reject(new Error(response.error));
|
|
20239
|
-
} else {
|
|
20240
|
-
entry.resolve(response.result);
|
|
20241
|
-
}
|
|
20242
|
-
}
|
|
20243
|
-
function rejectPendingOnClose(state) {
|
|
20244
|
-
for (const entry of state.pending.values()) {
|
|
20245
|
-
clearTimeout(entry.timer);
|
|
20246
|
-
entry.reject(new Error("NIP-46 WebSocket closed unexpectedly"));
|
|
20180
|
+
return void 0;
|
|
20247
20181
|
}
|
|
20248
|
-
state.pending.clear();
|
|
20249
20182
|
}
|
|
20250
|
-
function
|
|
20251
|
-
return
|
|
20252
|
-
state.ws = new WebSocket(state.relayUrl);
|
|
20253
|
-
state.ws.onopen = () => {
|
|
20254
|
-
state.ws.send(JSON.stringify([
|
|
20255
|
-
"REQ",
|
|
20256
|
-
state.subId,
|
|
20257
|
-
{ kinds: [24133], "#p": [state.localPubkey] }
|
|
20258
|
-
]));
|
|
20259
|
-
resolve();
|
|
20260
|
-
};
|
|
20261
|
-
state.ws.onmessage = (event) => {
|
|
20262
|
-
handleRelayMessage(state, event.data).catch(() => {
|
|
20263
|
-
});
|
|
20264
|
-
};
|
|
20265
|
-
state.ws.onerror = () => reject2(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
|
|
20266
|
-
state.ws.onclose = () => rejectPendingOnClose(state);
|
|
20267
|
-
});
|
|
20183
|
+
function escapeAttribute(value) {
|
|
20184
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
20268
20185
|
}
|
|
20269
|
-
function
|
|
20270
|
-
return
|
|
20271
|
-
encrypt: async (pubkey2, plaintext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_encrypt`, [pubkey2, plaintext]),
|
|
20272
|
-
decrypt: async (pubkey2, ciphertext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_decrypt`, [pubkey2, ciphertext])
|
|
20273
|
-
};
|
|
20186
|
+
function formatNappletManifestKinds() {
|
|
20187
|
+
return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
|
|
20274
20188
|
}
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
if (
|
|
20279
|
-
|
|
20280
|
-
} else if (result2 && typeof result2 === "object" && "pubkey" in result2) {
|
|
20281
|
-
state.authorizedPubkey = result2.pubkey;
|
|
20282
|
-
} else {
|
|
20283
|
-
const pkResult = await sendRequestWithId(state, crypto.randomUUID(), "get_public_key", []);
|
|
20284
|
-
state.authorizedPubkey = pkResult;
|
|
20189
|
+
|
|
20190
|
+
// src/browser-target-frame.ts
|
|
20191
|
+
function resolvePajaFrameEnvironment(hooks, identity) {
|
|
20192
|
+
if (!identity.dTag || !identity.aggregateHash) {
|
|
20193
|
+
throw new Error("Paja frame identity requires a dTag and aggregate hash");
|
|
20285
20194
|
}
|
|
20286
|
-
return
|
|
20195
|
+
return resolveShellEnvironment(hooks, identity);
|
|
20287
20196
|
}
|
|
20288
|
-
function
|
|
20197
|
+
function getTargetIdentity(config, resolvedTarget) {
|
|
20289
20198
|
return {
|
|
20290
|
-
|
|
20291
|
-
|
|
20292
|
-
|
|
20293
|
-
},
|
|
20294
|
-
signEvent: async (event) => {
|
|
20295
|
-
const result2 = await sendRequestWithId(state, crypto.randomUUID(), "sign_event", [JSON.stringify(event)]);
|
|
20296
|
-
if (typeof result2 === "string") {
|
|
20297
|
-
return JSON.parse(result2);
|
|
20298
|
-
}
|
|
20299
|
-
return result2;
|
|
20300
|
-
},
|
|
20301
|
-
nip04: createNipEncryptionMethods(state, "nip04"),
|
|
20302
|
-
nip44: createNipEncryptionMethods(state, "nip44")
|
|
20199
|
+
pubkey: "",
|
|
20200
|
+
dTag: resolvedTarget?.dTag ?? config.window.dTag,
|
|
20201
|
+
aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
|
|
20303
20202
|
};
|
|
20304
20203
|
}
|
|
20305
|
-
function
|
|
20306
|
-
const
|
|
20307
|
-
return {
|
|
20308
|
-
connect: () => connectNip46State(state, options.secret),
|
|
20309
|
-
getSigner: () => createNip46Signer(state),
|
|
20310
|
-
close: () => cleanupNip46State(state)
|
|
20311
|
-
};
|
|
20204
|
+
function getTargetOriginIdentity(config, resolvedTarget) {
|
|
20205
|
+
const target = getTargetIdentity(config, resolvedTarget);
|
|
20206
|
+
return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
|
|
20312
20207
|
}
|
|
20313
|
-
|
|
20314
|
-
|
|
20315
|
-
|
|
20316
|
-
|
|
20208
|
+
function registerFrameForGeneration(frame, config, generation, identity, environment, windowId = `${config.window.id}:${generation}`) {
|
|
20209
|
+
const win = frame.contentWindow;
|
|
20210
|
+
if (!win) return null;
|
|
20211
|
+
originRegistry.register(win, windowId, identity);
|
|
20212
|
+
originRegistry.setEnvironment(win, environment);
|
|
20213
|
+
return windowId;
|
|
20317
20214
|
}
|
|
20318
|
-
function
|
|
20319
|
-
|
|
20215
|
+
async function navigateFrame(frame, config, generation, adapter, resolvedTarget, windowId, isCurrent, onRegistered) {
|
|
20216
|
+
const identity = getTargetOriginIdentity(config, resolvedTarget);
|
|
20217
|
+
const environment = resolvePajaFrameEnvironment(adapter, identity);
|
|
20218
|
+
const domains = environment.capabilities.domains;
|
|
20219
|
+
if (config.target.mode === "runtime-pointer") {
|
|
20220
|
+
if (!resolvedTarget) {
|
|
20221
|
+
frame.removeAttribute("src");
|
|
20222
|
+
frame.srcdoc = "<!doctype html><html><body></body></html>";
|
|
20223
|
+
return null;
|
|
20224
|
+
}
|
|
20225
|
+
if (isCurrent && !isCurrent()) return null;
|
|
20226
|
+
const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
|
|
20227
|
+
onRegistered?.(registeredWindowId2);
|
|
20228
|
+
frame.removeAttribute("src");
|
|
20229
|
+
frame.srcdoc = injectNappletNamespacePrelude(
|
|
20230
|
+
injectPajaRuntimeCsp(
|
|
20231
|
+
resolvedTarget.indexHtml,
|
|
20232
|
+
connectOrigins([...resolvedTarget.relays, ...resolvedTarget.blossomServers])
|
|
20233
|
+
),
|
|
20234
|
+
{ domains }
|
|
20235
|
+
);
|
|
20236
|
+
return registeredWindowId2;
|
|
20237
|
+
}
|
|
20238
|
+
const html = await fetchTargetHtml();
|
|
20239
|
+
if (isCurrent && !isCurrent()) return null;
|
|
20240
|
+
const registeredWindowId = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
|
|
20241
|
+
onRegistered?.(registeredWindowId);
|
|
20242
|
+
frame.removeAttribute("src");
|
|
20243
|
+
frame.srcdoc = injectNappletNamespacePrelude(
|
|
20244
|
+
injectBaseHref(html, config.target.url),
|
|
20245
|
+
{ domains }
|
|
20246
|
+
);
|
|
20247
|
+
return registeredWindowId;
|
|
20320
20248
|
}
|
|
20321
|
-
function
|
|
20322
|
-
const
|
|
20323
|
-
|
|
20324
|
-
const signer = candidate;
|
|
20325
|
-
if (typeof signer.getPublicKey !== "function" || typeof signer.signEvent !== "function") return null;
|
|
20326
|
-
return signer;
|
|
20249
|
+
function renderTargetErrorHtml(error) {
|
|
20250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20251
|
+
return `<!doctype html><html><body><pre>${escapeHtml(message)}</pre></body></html>`;
|
|
20327
20252
|
}
|
|
20328
|
-
function
|
|
20329
|
-
|
|
20330
|
-
|
|
20331
|
-
|
|
20332
|
-
|
|
20333
|
-
|
|
20334
|
-
|
|
20335
|
-
|
|
20336
|
-
|
|
20253
|
+
function connectOrigins(urls) {
|
|
20254
|
+
const out = /* @__PURE__ */ new Set();
|
|
20255
|
+
for (const value of urls) {
|
|
20256
|
+
try {
|
|
20257
|
+
const url = new URL(value);
|
|
20258
|
+
if (url.protocol === "wss:" || url.protocol === "ws:") {
|
|
20259
|
+
out.add(value.replace(/\/$/, ""));
|
|
20260
|
+
} else {
|
|
20261
|
+
out.add(url.origin);
|
|
20337
20262
|
}
|
|
20338
|
-
|
|
20263
|
+
} catch {
|
|
20339
20264
|
}
|
|
20340
|
-
}
|
|
20265
|
+
}
|
|
20266
|
+
return [...out];
|
|
20341
20267
|
}
|
|
20342
|
-
function
|
|
20343
|
-
|
|
20344
|
-
|
|
20345
|
-
|
|
20346
|
-
|
|
20347
|
-
|
|
20348
|
-
|
|
20349
|
-
|
|
20268
|
+
async function fetchTargetHtml() {
|
|
20269
|
+
const response = await fetch(new URL("./__kehto/target.html", window.location.href), {
|
|
20270
|
+
cache: "no-store",
|
|
20271
|
+
headers: {
|
|
20272
|
+
accept: "text/html, application/xhtml+xml;q=0.9, */*;q=0.8"
|
|
20273
|
+
}
|
|
20274
|
+
});
|
|
20275
|
+
if (!response.ok) {
|
|
20276
|
+
throw new Error(`Paja target fetch failed: ${response.status} ${response.statusText}`);
|
|
20277
|
+
}
|
|
20278
|
+
return response.text();
|
|
20350
20279
|
}
|
|
20351
|
-
function
|
|
20352
|
-
|
|
20353
|
-
|
|
20354
|
-
|
|
20355
|
-
|
|
20356
|
-
|
|
20357
|
-
|
|
20358
|
-
}
|
|
20359
|
-
|
|
20360
|
-
|
|
20361
|
-
|
|
20362
|
-
|
|
20363
|
-
|
|
20364
|
-
|
|
20365
|
-
|
|
20366
|
-
|
|
20367
|
-
|
|
20368
|
-
|
|
20369
|
-
|
|
20370
|
-
|
|
20371
|
-
|
|
20372
|
-
|
|
20373
|
-
|
|
20374
|
-
|
|
20375
|
-
|
|
20376
|
-
|
|
20377
|
-
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
|
|
20381
|
-
|
|
20382
|
-
|
|
20383
|
-
|
|
20384
|
-
|
|
20385
|
-
|
|
20386
|
-
|
|
20387
|
-
|
|
20388
|
-
|
|
20389
|
-
|
|
20390
|
-
|
|
20391
|
-
|
|
20392
|
-
|
|
20393
|
-
|
|
20394
|
-
|
|
20395
|
-
|
|
20396
|
-
|
|
20397
|
-
|
|
20398
|
-
|
|
20399
|
-
|
|
20400
|
-
|
|
20401
|
-
|
|
20402
|
-
|
|
20403
|
-
|
|
20404
|
-
|
|
20405
|
-
|
|
20406
|
-
|
|
20407
|
-
|
|
20408
|
-
|
|
20409
|
-
|
|
20410
|
-
|
|
20411
|
-
|
|
20412
|
-
|
|
20413
|
-
|
|
20414
|
-
|
|
20415
|
-
|
|
20416
|
-
|
|
20417
|
-
|
|
20418
|
-
|
|
20280
|
+
function injectBaseHref(html, targetUrl) {
|
|
20281
|
+
const base = `<base href="${escapeAttribute2(targetUrl)}">`;
|
|
20282
|
+
if (/<head[^>]*>/i.test(html)) {
|
|
20283
|
+
return html.replace(/<head[^>]*>/i, (open) => `${open}${base}`);
|
|
20284
|
+
}
|
|
20285
|
+
if (/<html[^>]*>/i.test(html)) {
|
|
20286
|
+
return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${base}</head>`);
|
|
20287
|
+
}
|
|
20288
|
+
return `${base}${html}`;
|
|
20289
|
+
}
|
|
20290
|
+
function escapeAttribute2(value) {
|
|
20291
|
+
return escapeHtml(value).replaceAll('"', """);
|
|
20292
|
+
}
|
|
20293
|
+
function escapeHtml(value) {
|
|
20294
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
20295
|
+
}
|
|
20296
|
+
|
|
20297
|
+
// src/browser-devtools.ts
|
|
20298
|
+
var PAJA_LOG_LIMIT = 500;
|
|
20299
|
+
var proxyToReal = /* @__PURE__ */ new WeakMap();
|
|
20300
|
+
var realToProxy = /* @__PURE__ */ new WeakMap();
|
|
20301
|
+
var proxyContexts = /* @__PURE__ */ new WeakMap();
|
|
20302
|
+
function isNappletMessage(value) {
|
|
20303
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
|
|
20304
|
+
}
|
|
20305
|
+
function describeMessage(raw) {
|
|
20306
|
+
if (isNappletMessage(raw)) return raw.type;
|
|
20307
|
+
if (Array.isArray(raw) && typeof raw[0] === "string") return raw[0];
|
|
20308
|
+
return "unknown";
|
|
20309
|
+
}
|
|
20310
|
+
function previewMessage(raw) {
|
|
20311
|
+
try {
|
|
20312
|
+
return JSON.stringify(raw);
|
|
20313
|
+
} catch (error) {
|
|
20314
|
+
return `unserializable:${error instanceof Error ? error.message : String(error)}`;
|
|
20315
|
+
}
|
|
20316
|
+
}
|
|
20317
|
+
function compactMessageDetail(raw) {
|
|
20318
|
+
const detail = {};
|
|
20319
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
20320
|
+
if (key !== "type" && key !== "id" && value !== void 0) detail[key] = value;
|
|
20321
|
+
}
|
|
20322
|
+
if (Object.keys(detail).length === 0) return "";
|
|
20323
|
+
try {
|
|
20324
|
+
return JSON.stringify(detail);
|
|
20325
|
+
} catch {
|
|
20326
|
+
return "";
|
|
20327
|
+
}
|
|
20328
|
+
}
|
|
20329
|
+
function describeMessageDetail(raw) {
|
|
20330
|
+
if (!isNappletMessage(raw)) return "";
|
|
20331
|
+
const value = raw;
|
|
20332
|
+
for (const key of ["error", "message", "reason"]) {
|
|
20333
|
+
const detail = value[key];
|
|
20334
|
+
if (typeof detail === "string" && detail.length > 0) return detail;
|
|
20335
|
+
}
|
|
20336
|
+
return raw.type.endsWith(".error") ? compactMessageDetail(value) : "";
|
|
20337
|
+
}
|
|
20338
|
+
function domainEnabled(simulation, domain) {
|
|
20339
|
+
return simulation.capabilities.domains[domain] === true;
|
|
20340
|
+
}
|
|
20341
|
+
function appendPajaMessageLog(state, direction, raw, windowId) {
|
|
20342
|
+
if (!state) return;
|
|
20343
|
+
state.messageLog.push({
|
|
20344
|
+
index: state.messageLog.length,
|
|
20345
|
+
timestamp: Date.now(),
|
|
20346
|
+
direction,
|
|
20347
|
+
type: describeMessage(raw),
|
|
20348
|
+
windowId,
|
|
20349
|
+
preview: previewMessage(raw),
|
|
20350
|
+
detail: describeMessageDetail(raw)
|
|
20351
|
+
});
|
|
20352
|
+
if (state.messageLog.length > PAJA_LOG_LIMIT) {
|
|
20353
|
+
state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
|
|
20354
|
+
}
|
|
20355
|
+
renderPajaMessageLog(state);
|
|
20356
|
+
}
|
|
20357
|
+
function createPajaPostMessageProxy(realWin, state, windowId) {
|
|
20358
|
+
const existing = realToProxy.get(realWin);
|
|
20359
|
+
if (existing) {
|
|
20360
|
+
proxyContexts.set(existing, { state, windowId });
|
|
20361
|
+
return existing;
|
|
20362
|
+
}
|
|
20363
|
+
const proxy = new Proxy(realWin, {
|
|
20364
|
+
get(target, prop) {
|
|
20365
|
+
if (prop === "postMessage") {
|
|
20366
|
+
return (msg, targetOrigin, transfer) => {
|
|
20367
|
+
const context = proxyContexts.get(proxy);
|
|
20368
|
+
appendPajaMessageLog(
|
|
20369
|
+
context?.state ?? state,
|
|
20370
|
+
"shell->napplet",
|
|
20371
|
+
redactPajaMessageForLog(msg),
|
|
20372
|
+
context?.windowId ?? windowId
|
|
20373
|
+
);
|
|
20374
|
+
return target.postMessage(msg, targetOrigin, transfer);
|
|
20375
|
+
};
|
|
20419
20376
|
}
|
|
20420
|
-
update({ method: "nip46", status: "connecting", pubkey: null, relay: parsed.relay, error: null });
|
|
20421
20377
|
try {
|
|
20422
|
-
const
|
|
20423
|
-
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
});
|
|
20427
|
-
const pubkey2 = await client.connect();
|
|
20428
|
-
activeClient = client;
|
|
20429
|
-
activeSigner = createConfirmedSigner(client.getSigner(), options.confirmRequest);
|
|
20430
|
-
update({ method: "nip46", status: "connected", pubkey: pubkey2, relay: parsed.relay, error: null });
|
|
20431
|
-
} catch (error) {
|
|
20432
|
-
closeClient();
|
|
20433
|
-
activeSigner = null;
|
|
20434
|
-
update({ method: "nip46", status: "error", pubkey: null, relay: parsed.relay, error: toErrorMessage6(error) });
|
|
20378
|
+
const val = Reflect.get(target, prop, target);
|
|
20379
|
+
return typeof val === "function" ? val.bind(target) : val;
|
|
20380
|
+
} catch {
|
|
20381
|
+
return void 0;
|
|
20435
20382
|
}
|
|
20436
20383
|
}
|
|
20437
|
-
};
|
|
20384
|
+
});
|
|
20385
|
+
proxyToReal.set(proxy, realWin);
|
|
20386
|
+
realToProxy.set(realWin, proxy);
|
|
20387
|
+
proxyContexts.set(proxy, { state, windowId });
|
|
20388
|
+
return proxy;
|
|
20438
20389
|
}
|
|
20439
|
-
|
|
20440
|
-
|
|
20441
|
-
|
|
20442
|
-
|
|
20390
|
+
function redactPajaMessageForLog(message) {
|
|
20391
|
+
if (!isRecord6(message) || message.type !== "config.values") return message;
|
|
20392
|
+
return {
|
|
20393
|
+
type: "config.values",
|
|
20394
|
+
...typeof message.id === "string" ? { id: message.id } : {},
|
|
20395
|
+
values: "[redacted by host]"
|
|
20396
|
+
};
|
|
20443
20397
|
}
|
|
20444
|
-
function
|
|
20445
|
-
return
|
|
20398
|
+
function isRecord6(value) {
|
|
20399
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20446
20400
|
}
|
|
20447
|
-
function
|
|
20448
|
-
const
|
|
20449
|
-
|
|
20450
|
-
|
|
20451
|
-
|
|
20452
|
-
|
|
20453
|
-
const deny = document.getElementById("paja-confirmation-deny");
|
|
20454
|
-
const ready = dialog instanceof HTMLDialogElement && title instanceof HTMLElement && summary instanceof HTMLElement && details instanceof HTMLElement && approve instanceof HTMLButtonElement && deny instanceof HTMLButtonElement;
|
|
20455
|
-
const queue = [];
|
|
20456
|
-
let active = null;
|
|
20457
|
-
let previousFocus = null;
|
|
20458
|
-
let disposed = false;
|
|
20459
|
-
const pump = () => {
|
|
20460
|
-
if (!ready || disposed || active) return;
|
|
20461
|
-
active = queue.shift() ?? null;
|
|
20462
|
-
if (!active) return;
|
|
20463
|
-
const copy = describeConfirmation(active.request);
|
|
20464
|
-
title.textContent = copy.title;
|
|
20465
|
-
summary.textContent = copy.summary;
|
|
20466
|
-
details.textContent = copy.details;
|
|
20467
|
-
approve.textContent = copy.approveLabel;
|
|
20468
|
-
previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
20469
|
-
dialog.showModal();
|
|
20470
|
-
deny.focus();
|
|
20401
|
+
function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
|
|
20402
|
+
const originalGetIframeWindow = originRegistry2.getIframeWindow.bind(originRegistry2);
|
|
20403
|
+
originRegistry2.getIframeWindow = (windowId) => {
|
|
20404
|
+
const win = originalGetIframeWindow(windowId);
|
|
20405
|
+
if (!win) return null;
|
|
20406
|
+
return createPajaPostMessageProxy(win, stateRef(), windowId);
|
|
20471
20407
|
};
|
|
20472
|
-
const
|
|
20473
|
-
|
|
20474
|
-
|
|
20475
|
-
|
|
20476
|
-
|
|
20477
|
-
|
|
20478
|
-
if (allowed2) current.approve();
|
|
20479
|
-
else current.deny();
|
|
20480
|
-
previousFocus?.focus();
|
|
20481
|
-
previousFocus = null;
|
|
20482
|
-
queueMicrotask(pump);
|
|
20408
|
+
const originalGetWindowId = originRegistry2.getWindowId.bind(originRegistry2);
|
|
20409
|
+
originRegistry2.getWindowId = (win) => {
|
|
20410
|
+
const result2 = originalGetWindowId(win);
|
|
20411
|
+
if (result2) return result2;
|
|
20412
|
+
const real = proxyToReal.get(win);
|
|
20413
|
+
return real ? originalGetWindowId(real) : void 0;
|
|
20483
20414
|
};
|
|
20484
|
-
const
|
|
20485
|
-
|
|
20486
|
-
|
|
20487
|
-
|
|
20488
|
-
settle6(false);
|
|
20415
|
+
const originalGetIdentity = originRegistry2.getIdentity.bind(originRegistry2);
|
|
20416
|
+
originRegistry2.getIdentity = (win) => {
|
|
20417
|
+
const real = proxyToReal.get(win);
|
|
20418
|
+
return originalGetIdentity(win) ?? (real ? originalGetIdentity(real) : void 0);
|
|
20489
20419
|
};
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
}
|
|
20495
|
-
|
|
20496
|
-
|
|
20497
|
-
|
|
20498
|
-
|
|
20499
|
-
|
|
20500
|
-
|
|
20501
|
-
|
|
20420
|
+
const originalGetRegistrationId = originRegistry2.getRegistrationId.bind(originRegistry2);
|
|
20421
|
+
originRegistry2.getRegistrationId = (win) => {
|
|
20422
|
+
const real = proxyToReal.get(win);
|
|
20423
|
+
return originalGetRegistrationId(win) ?? (real ? originalGetRegistrationId(real) : void 0);
|
|
20424
|
+
};
|
|
20425
|
+
const originalGetEnvironment = originRegistry2.getEnvironment.bind(originRegistry2);
|
|
20426
|
+
originRegistry2.getEnvironment = (win) => {
|
|
20427
|
+
const real = proxyToReal.get(win);
|
|
20428
|
+
return originalGetEnvironment(win) ?? (real ? originalGetEnvironment(real) : void 0);
|
|
20429
|
+
};
|
|
20430
|
+
}
|
|
20431
|
+
function renderPajaDevtools(state, options) {
|
|
20432
|
+
renderInterfaceControls(state);
|
|
20433
|
+
renderAclControls(state, options.bridge);
|
|
20434
|
+
renderSignerStatus(state, options.devSignerPubkey);
|
|
20435
|
+
}
|
|
20436
|
+
function renderPajaMessageLog(state) {
|
|
20437
|
+
const container = document.getElementById("message-log");
|
|
20438
|
+
if (!container) return;
|
|
20439
|
+
const filter = state.messageFilter.trim().toLowerCase();
|
|
20440
|
+
const rows = state.messageLog.filter((entry) => filter.length === 0 || `${entry.direction} ${entry.type} ${entry.detail} ${entry.preview}`.toLowerCase().includes(filter)).slice(-120).map((entry) => {
|
|
20441
|
+
const row = document.createElement("div");
|
|
20442
|
+
row.className = "log-row";
|
|
20443
|
+
row.dataset.messageType = entry.type;
|
|
20444
|
+
if (entry.type.endsWith(".error")) row.dataset.error = "true";
|
|
20445
|
+
const dir = document.createElement("div");
|
|
20446
|
+
dir.className = "log-dir";
|
|
20447
|
+
dir.textContent = entry.direction;
|
|
20448
|
+
const body = document.createElement("div");
|
|
20449
|
+
body.className = "log-body";
|
|
20450
|
+
const type = document.createElement("div");
|
|
20451
|
+
type.className = "log-type";
|
|
20452
|
+
type.title = entry.preview;
|
|
20453
|
+
type.textContent = entry.type;
|
|
20454
|
+
body.append(type);
|
|
20455
|
+
if (entry.detail) {
|
|
20456
|
+
const detail = document.createElement("div");
|
|
20457
|
+
detail.className = "log-detail";
|
|
20458
|
+
detail.textContent = entry.detail;
|
|
20459
|
+
detail.title = entry.preview;
|
|
20460
|
+
body.append(detail);
|
|
20461
|
+
}
|
|
20462
|
+
row.append(dir, body);
|
|
20463
|
+
return row;
|
|
20464
|
+
});
|
|
20465
|
+
container.replaceChildren(...rows);
|
|
20466
|
+
container.scrollTop = container.scrollHeight;
|
|
20467
|
+
}
|
|
20468
|
+
function renderInterfaceControls(state) {
|
|
20469
|
+
const container = document.getElementById("interface-toggles");
|
|
20470
|
+
if (!container) return;
|
|
20471
|
+
container.replaceChildren(...PAJA_SIMULATION_DOMAINS.map((domain) => {
|
|
20472
|
+
const enabled = domainEnabled(state.simulation, domain);
|
|
20473
|
+
const button2 = document.createElement("button");
|
|
20474
|
+
button2.type = "button";
|
|
20475
|
+
button2.className = "toggle";
|
|
20476
|
+
button2.dataset.interfaceDomain = domain;
|
|
20477
|
+
button2.dataset.enabled = String(enabled);
|
|
20478
|
+
button2.textContent = domain;
|
|
20479
|
+
button2.title = `${domain} injection ${enabled ? "enabled" : "disabled"}`;
|
|
20480
|
+
button2.addEventListener("click", () => {
|
|
20481
|
+
state.setDomainEnabled(domain, !domainEnabled(state.simulation, domain));
|
|
20482
|
+
});
|
|
20483
|
+
return button2;
|
|
20484
|
+
}));
|
|
20485
|
+
}
|
|
20486
|
+
function renderAclControls(state, bridge) {
|
|
20487
|
+
const container = document.getElementById("acl-controls");
|
|
20488
|
+
if (!container) return;
|
|
20489
|
+
const identity = getTargetIdentity(state.config, state.resolvedTarget);
|
|
20490
|
+
container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
|
|
20491
|
+
const allowed2 = bridge?.runtime.aclState.check(
|
|
20492
|
+
identity.pubkey,
|
|
20493
|
+
identity.dTag,
|
|
20494
|
+
identity.aggregateHash,
|
|
20495
|
+
capability
|
|
20496
|
+
) ?? true;
|
|
20497
|
+
const button2 = document.createElement("button");
|
|
20498
|
+
button2.type = "button";
|
|
20499
|
+
button2.className = "toggle";
|
|
20500
|
+
button2.dataset.aclCapability = capability;
|
|
20501
|
+
button2.dataset.enabled = String(allowed2);
|
|
20502
|
+
button2.textContent = capability;
|
|
20503
|
+
button2.title = `${capability} ${allowed2 ? "allowed" : "blocked"}`;
|
|
20504
|
+
button2.addEventListener("click", () => state.setAclCapability(capability, !allowed2));
|
|
20505
|
+
return button2;
|
|
20506
|
+
}));
|
|
20507
|
+
}
|
|
20508
|
+
function renderSignerStatus(state, signerPubkey) {
|
|
20509
|
+
const el = document.getElementById("signer-status");
|
|
20510
|
+
const controls = document.getElementById("signer-controls");
|
|
20511
|
+
const clearConsent = document.getElementById("signer-consent-clear");
|
|
20512
|
+
if (!el || !controls || !(clearConsent instanceof HTMLButtonElement)) return;
|
|
20513
|
+
const fallbackPubkey = state.simulation.identity.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
|
|
20514
|
+
const pubkey2 = state.simulation.identity.pubkey || state.signer.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
|
|
20515
|
+
const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
|
|
20516
|
+
const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
|
|
20517
|
+
const prefix = state.signer.status === "disconnected" ? "no signer connected" : state.signer.status === "error" ? `${methodLabel} error: ${state.signer.error ?? "unknown error"} \xB7 fallback pubkey ${fallbackPubkey}` : `${methodLabel} ${state.signer.status} \xB7 pubkey ${pubkey2 || "(none)"}${relay}`;
|
|
20518
|
+
const consentSummary = state.signerConsentCount === 0 ? "signing asks every time" : `${state.signerConsentCount} remembered signing approval${state.signerConsentCount === 1 ? "" : "s"}`;
|
|
20519
|
+
el.textContent = `${prefix} \xB7 ${consentSummary} \xB7 publishing asks every time`;
|
|
20520
|
+
clearConsent.hidden = state.signerConsentCount === 0;
|
|
20521
|
+
clearConsent.textContent = `Forget ${state.signerConsentCount} remembered approval${state.signerConsentCount === 1 ? "" : "s"}`;
|
|
20522
|
+
clearConsent.onclick = () => state.clearSignerConsent();
|
|
20523
|
+
const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
|
|
20524
|
+
const devButton = document.createElement("button");
|
|
20525
|
+
devButton.type = "button";
|
|
20526
|
+
devButton.id = "signer-dev";
|
|
20527
|
+
devButton.dataset.active = String(state.signer.method === "dev");
|
|
20528
|
+
devButton.textContent = "Dev";
|
|
20529
|
+
devButton.addEventListener("click", () => state.useDevSigner());
|
|
20530
|
+
const nip07Button = document.createElement("button");
|
|
20531
|
+
nip07Button.type = "button";
|
|
20532
|
+
nip07Button.id = "signer-nip07";
|
|
20533
|
+
nip07Button.dataset.active = String(state.signer.method === "nip07");
|
|
20534
|
+
nip07Button.textContent = "NIP-07";
|
|
20535
|
+
nip07Button.addEventListener("click", () => void state.connectNip07());
|
|
20536
|
+
const bunkerInput = document.createElement("input");
|
|
20537
|
+
bunkerInput.id = "signer-bunker-uri";
|
|
20538
|
+
bunkerInput.type = "text";
|
|
20539
|
+
bunkerInput.autocomplete = "off";
|
|
20540
|
+
bunkerInput.placeholder = "bunker://...";
|
|
20541
|
+
bunkerInput.setAttribute("aria-label", "Bunker connection URI");
|
|
20542
|
+
bunkerInput.value = previousInput;
|
|
20543
|
+
const bunkerButton = document.createElement("button");
|
|
20544
|
+
bunkerButton.type = "button";
|
|
20545
|
+
bunkerButton.id = "signer-bunker";
|
|
20546
|
+
bunkerButton.dataset.active = String(state.signer.method === "nip46");
|
|
20547
|
+
bunkerButton.textContent = "Bunker";
|
|
20548
|
+
bunkerButton.addEventListener("click", () => void state.connectBunker(bunkerInput.value));
|
|
20549
|
+
controls.replaceChildren(devButton, nip07Button, bunkerInput, bunkerButton);
|
|
20550
|
+
}
|
|
20551
|
+
|
|
20552
|
+
// src/browser-nip46-client.ts
|
|
20553
|
+
function parseBunkerUri(uri) {
|
|
20554
|
+
if (!uri || typeof uri !== "string") return null;
|
|
20555
|
+
let rest;
|
|
20556
|
+
if (uri.startsWith("bunker://")) {
|
|
20557
|
+
rest = uri.slice("bunker://".length);
|
|
20558
|
+
} else if (uri.startsWith("nostrconnect://")) {
|
|
20559
|
+
rest = uri.slice("nostrconnect://".length);
|
|
20560
|
+
} else {
|
|
20561
|
+
return null;
|
|
20562
|
+
}
|
|
20563
|
+
const questionMark = rest.indexOf("?");
|
|
20564
|
+
const pubkey2 = questionMark !== -1 ? rest.slice(0, questionMark) : rest;
|
|
20565
|
+
const queryString = questionMark !== -1 ? rest.slice(questionMark + 1) : "";
|
|
20566
|
+
if (!pubkey2 || pubkey2.length !== 64 || !/^[0-9a-f]+$/u.test(pubkey2)) return null;
|
|
20567
|
+
const params = new URLSearchParams(queryString);
|
|
20568
|
+
const relay = params.get("relay");
|
|
20569
|
+
if (!relay) return null;
|
|
20570
|
+
return { pubkey: pubkey2, relay, secret: params.get("secret") ?? void 0 };
|
|
20571
|
+
}
|
|
20572
|
+
function createNip46ClientState(options) {
|
|
20573
|
+
const localSecretKey = normalizeLocalSecretKey(options.localSecretKey ?? generateSecretKey());
|
|
20574
|
+
const localPubkey = getPublicKey(localSecretKey);
|
|
20575
|
+
return {
|
|
20576
|
+
relayUrl: options.relayUrl,
|
|
20577
|
+
bunkerPubkey: options.bunkerPubkey,
|
|
20578
|
+
timeout: options.timeout ?? 3e4,
|
|
20579
|
+
localSecretKey,
|
|
20580
|
+
localPubkey,
|
|
20581
|
+
pending: /* @__PURE__ */ new Map(),
|
|
20582
|
+
subId: `paja-nip46-${localPubkey.substring(0, 8)}`,
|
|
20583
|
+
ws: null,
|
|
20584
|
+
authorizedPubkey: null
|
|
20585
|
+
};
|
|
20586
|
+
}
|
|
20587
|
+
function normalizeLocalSecretKey(secretKey) {
|
|
20588
|
+
if (secretKey.length !== 32) {
|
|
20589
|
+
throw new Error("NIP-46 local secret key must be 32 bytes");
|
|
20590
|
+
}
|
|
20591
|
+
return new Uint8Array(secretKey);
|
|
20592
|
+
}
|
|
20593
|
+
function cleanupNip46State(state) {
|
|
20594
|
+
for (const entry of state.pending.values()) {
|
|
20595
|
+
clearTimeout(entry.timer);
|
|
20596
|
+
entry.reject(new Error("NIP-46 client closed"));
|
|
20597
|
+
}
|
|
20598
|
+
state.pending.clear();
|
|
20599
|
+
if (state.ws && state.ws.readyState !== WebSocket.CLOSED) {
|
|
20600
|
+
state.ws.close();
|
|
20601
|
+
}
|
|
20602
|
+
state.ws = null;
|
|
20603
|
+
}
|
|
20604
|
+
async function sendRequestWithId(state, correlationId, method, params) {
|
|
20605
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) {
|
|
20606
|
+
throw new Error("NIP-46 WebSocket not open");
|
|
20607
|
+
}
|
|
20608
|
+
const payload = JSON.stringify({ id: correlationId, method, params });
|
|
20609
|
+
const encryptedContent = encrypt(payload, getConversationKey(state.localSecretKey, state.bunkerPubkey));
|
|
20610
|
+
const signedEvent = finalizeEvent({
|
|
20611
|
+
kind: 24133,
|
|
20612
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
20613
|
+
tags: [["p", state.bunkerPubkey]],
|
|
20614
|
+
content: encryptedContent
|
|
20615
|
+
}, state.localSecretKey);
|
|
20616
|
+
return new Promise((resolve, reject2) => {
|
|
20617
|
+
const timer = setTimeout(() => {
|
|
20618
|
+
state.pending.delete(correlationId);
|
|
20619
|
+
reject2(new Error(`NIP-46 request timed out: ${method}`));
|
|
20620
|
+
}, state.timeout);
|
|
20621
|
+
state.pending.set(correlationId, { resolve, reject: reject2, timer });
|
|
20622
|
+
state.ws.send(JSON.stringify(["EVENT", signedEvent]));
|
|
20623
|
+
});
|
|
20624
|
+
}
|
|
20625
|
+
async function handleRelayMessage(state, data) {
|
|
20626
|
+
let msg;
|
|
20627
|
+
try {
|
|
20628
|
+
msg = JSON.parse(data);
|
|
20629
|
+
} catch {
|
|
20630
|
+
return;
|
|
20631
|
+
}
|
|
20632
|
+
if (!Array.isArray(msg) || msg[0] !== "EVENT") return;
|
|
20633
|
+
const event = msg[2];
|
|
20634
|
+
if (!event || event.kind !== 24133 || typeof event.content !== "string") return;
|
|
20635
|
+
let response;
|
|
20636
|
+
try {
|
|
20637
|
+
response = JSON.parse(decrypt(event.content, getConversationKey(state.localSecretKey, state.bunkerPubkey)));
|
|
20638
|
+
} catch {
|
|
20639
|
+
return;
|
|
20640
|
+
}
|
|
20641
|
+
const entry = state.pending.get(response.id);
|
|
20642
|
+
if (!entry) return;
|
|
20643
|
+
clearTimeout(entry.timer);
|
|
20644
|
+
state.pending.delete(response.id);
|
|
20645
|
+
if (response.error) {
|
|
20646
|
+
entry.reject(new Error(response.error));
|
|
20647
|
+
} else {
|
|
20648
|
+
entry.resolve(response.result);
|
|
20649
|
+
}
|
|
20650
|
+
}
|
|
20651
|
+
function rejectPendingOnClose(state) {
|
|
20652
|
+
for (const entry of state.pending.values()) {
|
|
20653
|
+
clearTimeout(entry.timer);
|
|
20654
|
+
entry.reject(new Error("NIP-46 WebSocket closed unexpectedly"));
|
|
20655
|
+
}
|
|
20656
|
+
state.pending.clear();
|
|
20657
|
+
}
|
|
20658
|
+
function openWebSocket(state) {
|
|
20659
|
+
return new Promise((resolve, reject2) => {
|
|
20660
|
+
state.ws = new WebSocket(state.relayUrl);
|
|
20661
|
+
state.ws.onopen = () => {
|
|
20662
|
+
state.ws.send(JSON.stringify([
|
|
20663
|
+
"REQ",
|
|
20664
|
+
state.subId,
|
|
20665
|
+
{ kinds: [24133], "#p": [state.localPubkey] }
|
|
20666
|
+
]));
|
|
20667
|
+
resolve();
|
|
20668
|
+
};
|
|
20669
|
+
state.ws.onmessage = (event) => {
|
|
20670
|
+
handleRelayMessage(state, event.data).catch(() => {
|
|
20671
|
+
});
|
|
20672
|
+
};
|
|
20673
|
+
state.ws.onerror = () => reject2(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
|
|
20674
|
+
state.ws.onclose = () => rejectPendingOnClose(state);
|
|
20675
|
+
});
|
|
20676
|
+
}
|
|
20677
|
+
function createNipEncryptionMethods(state, prefix) {
|
|
20678
|
+
return {
|
|
20679
|
+
encrypt: async (pubkey2, plaintext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_encrypt`, [pubkey2, plaintext]),
|
|
20680
|
+
decrypt: async (pubkey2, ciphertext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_decrypt`, [pubkey2, ciphertext])
|
|
20681
|
+
};
|
|
20682
|
+
}
|
|
20683
|
+
async function connectNip46State(state, secret) {
|
|
20684
|
+
await openWebSocket(state);
|
|
20685
|
+
const result2 = await sendRequestWithId(state, crypto.randomUUID(), "connect", [state.bunkerPubkey, secret ?? ""]);
|
|
20686
|
+
if (typeof result2 === "string") {
|
|
20687
|
+
state.authorizedPubkey = result2;
|
|
20688
|
+
} else if (result2 && typeof result2 === "object" && "pubkey" in result2) {
|
|
20689
|
+
state.authorizedPubkey = result2.pubkey;
|
|
20690
|
+
} else {
|
|
20691
|
+
const pkResult = await sendRequestWithId(state, crypto.randomUUID(), "get_public_key", []);
|
|
20692
|
+
state.authorizedPubkey = pkResult;
|
|
20693
|
+
}
|
|
20694
|
+
return state.authorizedPubkey;
|
|
20695
|
+
}
|
|
20696
|
+
function createNip46Signer(state) {
|
|
20697
|
+
return {
|
|
20698
|
+
getPublicKey: async () => {
|
|
20699
|
+
if (state.authorizedPubkey) return state.authorizedPubkey;
|
|
20700
|
+
return sendRequestWithId(state, crypto.randomUUID(), "get_public_key", []);
|
|
20701
|
+
},
|
|
20702
|
+
signEvent: async (event) => {
|
|
20703
|
+
const result2 = await sendRequestWithId(state, crypto.randomUUID(), "sign_event", [JSON.stringify(event)]);
|
|
20704
|
+
if (typeof result2 === "string") {
|
|
20705
|
+
return JSON.parse(result2);
|
|
20706
|
+
}
|
|
20707
|
+
return result2;
|
|
20708
|
+
},
|
|
20709
|
+
nip04: createNipEncryptionMethods(state, "nip04"),
|
|
20710
|
+
nip44: createNipEncryptionMethods(state, "nip44")
|
|
20711
|
+
};
|
|
20712
|
+
}
|
|
20713
|
+
function createNip46Client(options) {
|
|
20714
|
+
const state = createNip46ClientState(options);
|
|
20715
|
+
return {
|
|
20716
|
+
connect: () => connectNip46State(state, options.secret),
|
|
20717
|
+
getSigner: () => createNip46Signer(state),
|
|
20718
|
+
close: () => cleanupNip46State(state)
|
|
20719
|
+
};
|
|
20720
|
+
}
|
|
20721
|
+
|
|
20722
|
+
// src/browser-signers.ts
|
|
20723
|
+
function cloneState(state) {
|
|
20724
|
+
return { ...state };
|
|
20725
|
+
}
|
|
20726
|
+
function toErrorMessage6(error) {
|
|
20727
|
+
return error instanceof Error ? error.message : String(error);
|
|
20728
|
+
}
|
|
20729
|
+
function readNip07Signer() {
|
|
20730
|
+
const candidate = globalThis.nostr;
|
|
20731
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
20732
|
+
const signer = candidate;
|
|
20733
|
+
if (typeof signer.getPublicKey !== "function" || typeof signer.signEvent !== "function") return null;
|
|
20734
|
+
return signer;
|
|
20735
|
+
}
|
|
20736
|
+
function createConfirmedSigner(signer, confirmRequest, source) {
|
|
20737
|
+
return {
|
|
20738
|
+
...signer,
|
|
20739
|
+
async signEvent(event) {
|
|
20740
|
+
if (typeof signer.signEvent !== "function") {
|
|
20741
|
+
throw new Error("Signer does not support event signing");
|
|
20742
|
+
}
|
|
20743
|
+
const signerContext = await resolveSignerContext(signer, source);
|
|
20744
|
+
if (!await confirmRequest({
|
|
20745
|
+
action: "sign",
|
|
20746
|
+
event,
|
|
20747
|
+
...signerContext ? { signerContext } : {}
|
|
20748
|
+
})) {
|
|
20749
|
+
throw new Error("Paja signing request denied");
|
|
20750
|
+
}
|
|
20751
|
+
return signer.signEvent(event);
|
|
20752
|
+
}
|
|
20753
|
+
};
|
|
20754
|
+
}
|
|
20755
|
+
async function resolveSignerContext(signer, source) {
|
|
20756
|
+
if (!source || typeof signer.getPublicKey !== "function") return void 0;
|
|
20757
|
+
try {
|
|
20758
|
+
const signerPubkey = await signer.getPublicKey();
|
|
20759
|
+
if (!/^[0-9a-f]{64}$/i.test(signerPubkey)) return void 0;
|
|
20760
|
+
return { ...source, signerPubkey };
|
|
20761
|
+
} catch {
|
|
20762
|
+
return void 0;
|
|
20763
|
+
}
|
|
20764
|
+
}
|
|
20765
|
+
function createNip07Adapter(signer) {
|
|
20766
|
+
return {
|
|
20767
|
+
getPublicKey: signer.getPublicKey?.bind(signer),
|
|
20768
|
+
signEvent: signer.signEvent?.bind(signer),
|
|
20769
|
+
getRelays: signer.getRelays?.bind(signer),
|
|
20770
|
+
nip04: signer.nip04,
|
|
20771
|
+
nip44: signer.nip44
|
|
20772
|
+
};
|
|
20773
|
+
}
|
|
20774
|
+
function createPajaSignerController(options) {
|
|
20775
|
+
let state = {
|
|
20776
|
+
method: "none",
|
|
20777
|
+
status: "disconnected",
|
|
20778
|
+
pubkey: null,
|
|
20779
|
+
relay: null,
|
|
20780
|
+
error: null
|
|
20781
|
+
};
|
|
20782
|
+
let activeSigner = null;
|
|
20783
|
+
let activeClient = null;
|
|
20784
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
20785
|
+
const update = (next) => {
|
|
20786
|
+
state = { ...state, ...next };
|
|
20787
|
+
for (const listener of listeners) listener();
|
|
20788
|
+
options.onChange(cloneState(state));
|
|
20789
|
+
};
|
|
20790
|
+
const closeClient = () => {
|
|
20791
|
+
activeClient?.close();
|
|
20792
|
+
activeClient = null;
|
|
20793
|
+
};
|
|
20794
|
+
return {
|
|
20795
|
+
getState: () => cloneState(state),
|
|
20796
|
+
getSigner(source) {
|
|
20797
|
+
if (!activeSigner) return null;
|
|
20798
|
+
return createConfirmedSigner(activeSigner, options.confirmRequest, source);
|
|
20799
|
+
},
|
|
20800
|
+
getMethod: () => state.method,
|
|
20801
|
+
getPubkey: () => state.pubkey,
|
|
20802
|
+
subscribe(listener) {
|
|
20803
|
+
listeners.add(listener);
|
|
20804
|
+
return () => listeners.delete(listener);
|
|
20805
|
+
},
|
|
20806
|
+
useDevSigner() {
|
|
20807
|
+
closeClient();
|
|
20808
|
+
activeSigner = null;
|
|
20809
|
+
update({
|
|
20810
|
+
method: "dev",
|
|
20811
|
+
status: "connected",
|
|
20812
|
+
pubkey: null,
|
|
20813
|
+
relay: null,
|
|
20814
|
+
error: null
|
|
20815
|
+
});
|
|
20816
|
+
},
|
|
20817
|
+
async connectNip07() {
|
|
20818
|
+
closeClient();
|
|
20819
|
+
activeSigner = null;
|
|
20820
|
+
update({ method: "nip07", status: "connecting", pubkey: null, relay: null, error: null });
|
|
20821
|
+
try {
|
|
20822
|
+
const signer = readNip07Signer();
|
|
20823
|
+
if (!signer) throw new Error("NIP-07 signer not found");
|
|
20824
|
+
const pubkey2 = await signer.getPublicKey();
|
|
20825
|
+
activeSigner = createNip07Adapter(signer);
|
|
20826
|
+
update({ method: "nip07", status: "connected", pubkey: pubkey2, relay: null, error: null });
|
|
20827
|
+
} catch (error) {
|
|
20828
|
+
activeSigner = null;
|
|
20829
|
+
update({ method: "nip07", status: "error", pubkey: null, relay: null, error: toErrorMessage6(error) });
|
|
20830
|
+
}
|
|
20831
|
+
},
|
|
20832
|
+
async connectBunker(uri) {
|
|
20833
|
+
closeClient();
|
|
20834
|
+
activeSigner = null;
|
|
20835
|
+
const parsed = parseBunkerUri(uri.trim());
|
|
20836
|
+
if (!parsed) {
|
|
20837
|
+
update({
|
|
20838
|
+
method: "nip46",
|
|
20839
|
+
status: "error",
|
|
20840
|
+
pubkey: null,
|
|
20841
|
+
relay: null,
|
|
20842
|
+
error: "Enter a bunker:// or nostrconnect:// URI with a relay parameter"
|
|
20843
|
+
});
|
|
20844
|
+
return;
|
|
20845
|
+
}
|
|
20846
|
+
update({ method: "nip46", status: "connecting", pubkey: null, relay: parsed.relay, error: null });
|
|
20847
|
+
try {
|
|
20848
|
+
const client = createNip46Client({
|
|
20849
|
+
relayUrl: parsed.relay,
|
|
20850
|
+
bunkerPubkey: parsed.pubkey,
|
|
20851
|
+
secret: parsed.secret
|
|
20852
|
+
});
|
|
20853
|
+
const pubkey2 = await client.connect();
|
|
20854
|
+
activeClient = client;
|
|
20855
|
+
activeSigner = client.getSigner();
|
|
20856
|
+
update({ method: "nip46", status: "connected", pubkey: pubkey2, relay: parsed.relay, error: null });
|
|
20857
|
+
} catch (error) {
|
|
20858
|
+
closeClient();
|
|
20859
|
+
activeSigner = null;
|
|
20860
|
+
update({ method: "nip46", status: "error", pubkey: null, relay: parsed.relay, error: toErrorMessage6(error) });
|
|
20861
|
+
}
|
|
20862
|
+
}
|
|
20863
|
+
};
|
|
20864
|
+
}
|
|
20865
|
+
|
|
20866
|
+
// src/browser-signer-consent.ts
|
|
20867
|
+
var PAJA_SIGNER_CONSENT_STORAGE_KEY = "kehto:paja:signer-consent:v1";
|
|
20868
|
+
var PAJA_SIGNER_CONSENT_VERSION = 1;
|
|
20869
|
+
var MAX_CONSENT_SUBJECTS = 128;
|
|
20870
|
+
var MAX_KINDS_PER_SUBJECT = 128;
|
|
20871
|
+
var MAX_IDENTITY_FIELD_LENGTH = 512;
|
|
20872
|
+
function createPajaSignerConsentStore(storage = readPajaSignerConsentStorage()) {
|
|
20873
|
+
let grants = readConsentDocument(storage).grants;
|
|
20874
|
+
const persist = () => {
|
|
20875
|
+
if (!storage) return;
|
|
20876
|
+
const document2 = {
|
|
20877
|
+
version: PAJA_SIGNER_CONSENT_VERSION,
|
|
20878
|
+
grants
|
|
20879
|
+
};
|
|
20880
|
+
try {
|
|
20881
|
+
storage.setItem(PAJA_SIGNER_CONSENT_STORAGE_KEY, JSON.stringify(document2));
|
|
20882
|
+
} catch {
|
|
20883
|
+
}
|
|
20884
|
+
};
|
|
20885
|
+
const recordFor = (context) => {
|
|
20886
|
+
const subject = normalizeContext(context);
|
|
20887
|
+
if (!subject) return null;
|
|
20888
|
+
return grants.find((grant2) => sameSubject(grant2, subject)) ?? null;
|
|
20889
|
+
};
|
|
20890
|
+
const ensureRecord = (context) => {
|
|
20891
|
+
const subject = normalizeContext(context);
|
|
20892
|
+
if (!subject) return null;
|
|
20893
|
+
const existing = grants.find((grant2) => sameSubject(grant2, subject));
|
|
20894
|
+
if (existing) return existing;
|
|
20895
|
+
if (grants.length >= MAX_CONSENT_SUBJECTS) return null;
|
|
20896
|
+
const created = { ...subject, trusted: false, kinds: [] };
|
|
20897
|
+
grants = [...grants, created];
|
|
20898
|
+
return created;
|
|
20899
|
+
};
|
|
20900
|
+
return {
|
|
20901
|
+
match(context, kind) {
|
|
20902
|
+
const normalizedKind = normalizeKind(kind);
|
|
20903
|
+
if (normalizedKind === null) return null;
|
|
20904
|
+
const record = recordFor(context);
|
|
20905
|
+
if (!record) return null;
|
|
20906
|
+
if (record.trusted) return "napplet";
|
|
20907
|
+
return record.kinds.includes(normalizedKind) ? "kind" : null;
|
|
20908
|
+
},
|
|
20909
|
+
rememberKind(context, kind) {
|
|
20910
|
+
const normalizedKind = normalizeKind(kind);
|
|
20911
|
+
if (normalizedKind === null) return false;
|
|
20912
|
+
const record = ensureRecord(context);
|
|
20913
|
+
if (!record) return false;
|
|
20914
|
+
if (record.trusted || record.kinds.includes(normalizedKind)) return true;
|
|
20915
|
+
if (record.kinds.length >= MAX_KINDS_PER_SUBJECT) return false;
|
|
20916
|
+
record.kinds = [...record.kinds, normalizedKind].sort((left, right) => left - right);
|
|
20917
|
+
persist();
|
|
20918
|
+
return true;
|
|
20919
|
+
},
|
|
20920
|
+
trustNapplet(context) {
|
|
20921
|
+
const record = ensureRecord(context);
|
|
20922
|
+
if (!record) return false;
|
|
20923
|
+
if (!record.trusted || record.kinds.length > 0) {
|
|
20924
|
+
record.trusted = true;
|
|
20925
|
+
record.kinds = [];
|
|
20926
|
+
persist();
|
|
20927
|
+
}
|
|
20928
|
+
return true;
|
|
20929
|
+
},
|
|
20930
|
+
clear() {
|
|
20931
|
+
const previous = grants;
|
|
20932
|
+
grants = [];
|
|
20933
|
+
if (!storage) return true;
|
|
20934
|
+
try {
|
|
20935
|
+
storage.removeItem(PAJA_SIGNER_CONSENT_STORAGE_KEY);
|
|
20936
|
+
return true;
|
|
20937
|
+
} catch {
|
|
20938
|
+
try {
|
|
20939
|
+
const empty = {
|
|
20940
|
+
version: PAJA_SIGNER_CONSENT_VERSION,
|
|
20941
|
+
grants: []
|
|
20942
|
+
};
|
|
20943
|
+
storage.setItem(PAJA_SIGNER_CONSENT_STORAGE_KEY, JSON.stringify(empty));
|
|
20944
|
+
return true;
|
|
20945
|
+
} catch {
|
|
20946
|
+
grants = previous;
|
|
20947
|
+
return false;
|
|
20948
|
+
}
|
|
20949
|
+
}
|
|
20950
|
+
},
|
|
20951
|
+
count() {
|
|
20952
|
+
return grants.reduce((total, grant2) => total + (grant2.trusted ? 1 : grant2.kinds.length), 0);
|
|
20953
|
+
}
|
|
20954
|
+
};
|
|
20955
|
+
}
|
|
20956
|
+
function isPajaSignerConsentContext(context) {
|
|
20957
|
+
return normalizeContext(context) !== null;
|
|
20958
|
+
}
|
|
20959
|
+
function readPajaSignerConsentStorage() {
|
|
20960
|
+
if (typeof window === "undefined") return null;
|
|
20961
|
+
try {
|
|
20962
|
+
return window.localStorage;
|
|
20963
|
+
} catch {
|
|
20964
|
+
return null;
|
|
20965
|
+
}
|
|
20966
|
+
}
|
|
20967
|
+
function readConsentDocument(storage) {
|
|
20968
|
+
const empty = () => ({ version: PAJA_SIGNER_CONSENT_VERSION, grants: [] });
|
|
20969
|
+
if (!storage) return empty();
|
|
20970
|
+
try {
|
|
20971
|
+
const raw = storage.getItem(PAJA_SIGNER_CONSENT_STORAGE_KEY);
|
|
20972
|
+
if (!raw) return empty();
|
|
20973
|
+
const parsed = JSON.parse(raw);
|
|
20974
|
+
if (parsed.version !== PAJA_SIGNER_CONSENT_VERSION || !Array.isArray(parsed.grants)) return empty();
|
|
20975
|
+
const grants = [];
|
|
20976
|
+
for (const candidate of parsed.grants.slice(0, MAX_CONSENT_SUBJECTS)) {
|
|
20977
|
+
const record = normalizeRecord(candidate);
|
|
20978
|
+
if (!record || grants.some((existing) => sameSubject(existing, record))) continue;
|
|
20979
|
+
grants.push(record);
|
|
20980
|
+
}
|
|
20981
|
+
return { version: PAJA_SIGNER_CONSENT_VERSION, grants };
|
|
20982
|
+
} catch {
|
|
20983
|
+
return empty();
|
|
20984
|
+
}
|
|
20985
|
+
}
|
|
20986
|
+
function normalizeRecord(value) {
|
|
20987
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
20988
|
+
const candidate = value;
|
|
20989
|
+
const subject = normalizeSubject(
|
|
20990
|
+
candidate.signerPubkey,
|
|
20991
|
+
candidate.dTag,
|
|
20992
|
+
candidate.aggregateHash,
|
|
20993
|
+
candidate.runtimeScope
|
|
20994
|
+
);
|
|
20995
|
+
if (!subject || typeof candidate.trusted !== "boolean" || !Array.isArray(candidate.kinds)) return null;
|
|
20996
|
+
const kinds = [...new Set(candidate.kinds.map((kind) => normalizeKind(kind)).filter((kind) => kind !== null))].slice(0, MAX_KINDS_PER_SUBJECT).sort((left, right) => left - right);
|
|
20997
|
+
return {
|
|
20998
|
+
...subject,
|
|
20999
|
+
trusted: candidate.trusted,
|
|
21000
|
+
kinds: candidate.trusted ? [] : kinds
|
|
21001
|
+
};
|
|
21002
|
+
}
|
|
21003
|
+
function normalizeContext(context) {
|
|
21004
|
+
return normalizeSubject(
|
|
21005
|
+
context.signerPubkey,
|
|
21006
|
+
context.napplet.dTag,
|
|
21007
|
+
context.napplet.aggregateHash,
|
|
21008
|
+
context.runtimeScope
|
|
21009
|
+
);
|
|
21010
|
+
}
|
|
21011
|
+
function normalizeSubject(signerPubkey, dTag, aggregateHash, runtimeScope) {
|
|
21012
|
+
if (typeof signerPubkey !== "string" || !/^[0-9a-f]{64}$/i.test(signerPubkey)) return null;
|
|
21013
|
+
if (!validIdentityField(dTag) || !validIdentityField(aggregateHash) || !validIdentityField(runtimeScope)) return null;
|
|
21014
|
+
return {
|
|
21015
|
+
signerPubkey: signerPubkey.toLowerCase(),
|
|
21016
|
+
dTag,
|
|
21017
|
+
aggregateHash,
|
|
21018
|
+
runtimeScope
|
|
21019
|
+
};
|
|
21020
|
+
}
|
|
21021
|
+
function validIdentityField(value) {
|
|
21022
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_IDENTITY_FIELD_LENGTH;
|
|
21023
|
+
}
|
|
21024
|
+
function normalizeKind(value) {
|
|
21025
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 65535 ? value : null;
|
|
21026
|
+
}
|
|
21027
|
+
function sameSubject(left, right) {
|
|
21028
|
+
return left.signerPubkey === right.signerPubkey && left.dTag === right.dTag && left.aggregateHash === right.aggregateHash && left.runtimeScope === right.runtimeScope;
|
|
21029
|
+
}
|
|
21030
|
+
|
|
21031
|
+
// src/browser-host-signer.ts
|
|
21032
|
+
function confirmationDetails(...lines) {
|
|
21033
|
+
return lines.filter((line) => line !== void 0).join("\n");
|
|
21034
|
+
}
|
|
21035
|
+
function confirmationCopy(title, summary, approveLabel, ...details) {
|
|
21036
|
+
return { title, summary, details: confirmationDetails(...details), approveLabel };
|
|
21037
|
+
}
|
|
21038
|
+
function createPajaConfirmationController(getState, options = {}) {
|
|
21039
|
+
const dialog = document.getElementById("paja-confirmation-dialog");
|
|
21040
|
+
const title = document.getElementById("paja-confirmation-title");
|
|
21041
|
+
const summary = document.getElementById("paja-confirmation-summary");
|
|
21042
|
+
const details = document.getElementById("paja-confirmation-details");
|
|
21043
|
+
const approve = document.getElementById("paja-confirmation-approve");
|
|
21044
|
+
const deny = document.getElementById("paja-confirmation-deny");
|
|
21045
|
+
const signerConsent = document.getElementById("paja-signer-consent");
|
|
21046
|
+
const signerConsentOnce = document.getElementById("paja-signer-consent-once");
|
|
21047
|
+
const signerConsentKind = document.getElementById("paja-signer-consent-kind");
|
|
21048
|
+
const signerConsentNapplet = document.getElementById("paja-signer-consent-napplet");
|
|
21049
|
+
const signerConsentKindValue = document.getElementById("paja-signer-consent-kind-value");
|
|
21050
|
+
const signerConsentNappletValue = document.getElementById("paja-signer-consent-napplet-value");
|
|
21051
|
+
const ready = dialog instanceof HTMLDialogElement && title instanceof HTMLElement && summary instanceof HTMLElement && details instanceof HTMLElement && approve instanceof HTMLButtonElement && deny instanceof HTMLButtonElement;
|
|
21052
|
+
const signerConsentReady = signerConsent instanceof HTMLFieldSetElement && signerConsentOnce instanceof HTMLInputElement && signerConsentKind instanceof HTMLInputElement && signerConsentNapplet instanceof HTMLInputElement && signerConsentKindValue instanceof HTMLElement && signerConsentNappletValue instanceof HTMLElement;
|
|
21053
|
+
const consentStore = createPajaSignerConsentStore(
|
|
21054
|
+
options.storage === void 0 ? void 0 : options.storage
|
|
21055
|
+
);
|
|
21056
|
+
const queue = [];
|
|
21057
|
+
let active = null;
|
|
21058
|
+
let previousFocus = null;
|
|
21059
|
+
let disposed = false;
|
|
21060
|
+
const reportSignerConsentChange = () => {
|
|
21061
|
+
options.onSignerConsentChange?.(consentStore.count());
|
|
21062
|
+
};
|
|
21063
|
+
const configureSignerConsent = (request) => {
|
|
21064
|
+
if (!signerConsentReady) return;
|
|
21065
|
+
signerConsent.hidden = true;
|
|
21066
|
+
signerConsentOnce.checked = true;
|
|
21067
|
+
signerConsentKind.checked = false;
|
|
21068
|
+
signerConsentNapplet.checked = false;
|
|
21069
|
+
const candidate = signerConsentCandidate(request);
|
|
21070
|
+
if (!candidate) return;
|
|
21071
|
+
signerConsent.hidden = false;
|
|
21072
|
+
signerConsentKindValue.textContent = String(candidate.kind);
|
|
21073
|
+
signerConsentNappletValue.textContent = candidate.context.napplet.dTag;
|
|
21074
|
+
};
|
|
21075
|
+
const rememberSignerChoice = (request) => {
|
|
21076
|
+
if (!signerConsentReady) return "once";
|
|
21077
|
+
const candidate = signerConsentCandidate(request);
|
|
21078
|
+
if (!candidate) return "once";
|
|
21079
|
+
if (signerConsentNapplet.checked && consentStore.trustNapplet(candidate.context)) {
|
|
21080
|
+
reportSignerConsentChange();
|
|
21081
|
+
return "trust-napplet";
|
|
21082
|
+
}
|
|
21083
|
+
if (signerConsentKind.checked && consentStore.rememberKind(candidate.context, candidate.kind)) {
|
|
21084
|
+
reportSignerConsentChange();
|
|
21085
|
+
return "remember-kind";
|
|
21086
|
+
}
|
|
21087
|
+
return "once";
|
|
21088
|
+
};
|
|
21089
|
+
const rememberedSignerChoice = (request) => {
|
|
21090
|
+
const candidate = signerConsentCandidate(request);
|
|
21091
|
+
return candidate ? consentStore.match(candidate.context, candidate.kind) : null;
|
|
21092
|
+
};
|
|
21093
|
+
const pump = () => {
|
|
21094
|
+
if (!ready || disposed || active) return;
|
|
21095
|
+
while (!active && queue.length > 0) {
|
|
21096
|
+
const candidate = queue.shift() ?? null;
|
|
21097
|
+
if (!candidate) break;
|
|
21098
|
+
const remembered = rememberedSignerChoice(candidate.request);
|
|
21099
|
+
if (!remembered) {
|
|
21100
|
+
active = candidate;
|
|
21101
|
+
break;
|
|
21102
|
+
}
|
|
21103
|
+
recordPajaConfirmation(
|
|
21104
|
+
getState(),
|
|
21105
|
+
candidate.request,
|
|
21106
|
+
true,
|
|
21107
|
+
remembered === "kind" ? "remembered-kind" : "remembered-napplet"
|
|
21108
|
+
);
|
|
21109
|
+
candidate.approve();
|
|
21110
|
+
}
|
|
21111
|
+
if (!active) return;
|
|
21112
|
+
const copy = describeConfirmation(active.request);
|
|
21113
|
+
title.textContent = copy.title;
|
|
21114
|
+
summary.textContent = copy.summary;
|
|
21115
|
+
details.textContent = copy.details;
|
|
21116
|
+
approve.textContent = copy.approveLabel;
|
|
21117
|
+
configureSignerConsent(active.request);
|
|
21118
|
+
previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
21119
|
+
dialog.showModal();
|
|
21120
|
+
deny.focus();
|
|
21121
|
+
};
|
|
21122
|
+
const settle6 = (allowed2) => {
|
|
21123
|
+
const current = active;
|
|
21124
|
+
if (!current) return;
|
|
21125
|
+
active = null;
|
|
21126
|
+
if (ready && dialog.open) dialog.close();
|
|
21127
|
+
const approvalMode = allowed2 ? rememberSignerChoice(current.request) : void 0;
|
|
21128
|
+
recordPajaConfirmation(getState(), current.request, allowed2, approvalMode);
|
|
21129
|
+
if (allowed2) current.approve();
|
|
21130
|
+
else current.deny();
|
|
21131
|
+
previousFocus?.focus();
|
|
21132
|
+
previousFocus = null;
|
|
21133
|
+
queueMicrotask(pump);
|
|
21134
|
+
};
|
|
21135
|
+
const onApprove = () => settle6(true);
|
|
21136
|
+
const onDeny = () => settle6(false);
|
|
21137
|
+
const onCancel = (event) => {
|
|
21138
|
+
event.preventDefault();
|
|
21139
|
+
settle6(false);
|
|
21140
|
+
};
|
|
21141
|
+
if (ready) {
|
|
21142
|
+
approve.addEventListener("click", onApprove);
|
|
21143
|
+
deny.addEventListener("click", onDeny);
|
|
21144
|
+
dialog.addEventListener("cancel", onCancel);
|
|
21145
|
+
}
|
|
21146
|
+
return {
|
|
21147
|
+
confirm(request) {
|
|
21148
|
+
if (!ready || disposed) {
|
|
21149
|
+
recordPajaConfirmation(getState(), request, false);
|
|
21150
|
+
return false;
|
|
21151
|
+
}
|
|
21152
|
+
return new Promise((resolve) => {
|
|
20502
21153
|
queue.push({ request, approve: () => resolve(true), deny: () => resolve(false) });
|
|
20503
21154
|
pump();
|
|
20504
21155
|
});
|
|
@@ -20528,6 +21179,12 @@ function createPajaConfirmationController(getState) {
|
|
|
20528
21179
|
});
|
|
20529
21180
|
}
|
|
20530
21181
|
},
|
|
21182
|
+
clearSignerConsent() {
|
|
21183
|
+
const cleared = consentStore.clear();
|
|
21184
|
+
reportSignerConsentChange();
|
|
21185
|
+
return cleared;
|
|
21186
|
+
},
|
|
21187
|
+
getSignerConsentCount: () => consentStore.count(),
|
|
20531
21188
|
dispose() {
|
|
20532
21189
|
if (disposed) return;
|
|
20533
21190
|
disposed = true;
|
|
@@ -20544,6 +21201,13 @@ function createPajaConfirmationController(getState) {
|
|
|
20544
21201
|
}
|
|
20545
21202
|
};
|
|
20546
21203
|
}
|
|
21204
|
+
function signerConsentCandidate(request) {
|
|
21205
|
+
if (request.action !== "sign" || !request.signerContext) return null;
|
|
21206
|
+
if (!isPajaSignerConsentContext(request.signerContext)) return null;
|
|
21207
|
+
const kind = request.event.kind;
|
|
21208
|
+
if (typeof kind !== "number" || !Number.isSafeInteger(kind) || kind < 0 || kind > 65535) return null;
|
|
21209
|
+
return { context: request.signerContext, kind };
|
|
21210
|
+
}
|
|
20547
21211
|
function describeConfirmation(request) {
|
|
20548
21212
|
if (request.action === "upload") {
|
|
20549
21213
|
return confirmationCopy(
|
|
@@ -20584,730 +21248,499 @@ function describeConfirmation(request) {
|
|
|
20584
21248
|
return confirmationCopy(
|
|
20585
21249
|
"Allow notifications?",
|
|
20586
21250
|
`${request.napplet.dTag} requests permission to show shell notifications.`,
|
|
20587
|
-
"Allow notifications",
|
|
20588
|
-
`Napplet: ${request.napplet.dTag} (${request.windowId})`,
|
|
20589
|
-
`Identity: ${request.napplet.aggregateHash}`,
|
|
20590
|
-
`Channel: ${request.channel ?? "all notifications"}`
|
|
20591
|
-
);
|
|
20592
|
-
}
|
|
20593
|
-
if (request.action === "webrtc") {
|
|
20594
|
-
return confirmationCopy(
|
|
20595
|
-
"Start a peer connection?",
|
|
20596
|
-
`${request.napplet.dTag} requests a WebRTC data session.`,
|
|
20597
|
-
"Connect",
|
|
20598
|
-
`Napplet: ${request.napplet.dTag} (${request.windowId})`,
|
|
20599
|
-
`Scope: ${request.scope}`,
|
|
20600
|
-
request.warning
|
|
20601
|
-
);
|
|
20602
|
-
}
|
|
20603
|
-
if (request.action === "dm") {
|
|
20604
|
-
return confirmationCopy(
|
|
20605
|
-
"Send this encrypted message?",
|
|
20606
|
-
"A napplet requests NIP-17 direct-message delivery.",
|
|
20607
|
-
"Send message",
|
|
20608
|
-
`Recipients: ${request.recipients.join(", ")}`,
|
|
20609
|
-
`Content: ${request.content.slice(0, 240)}`,
|
|
20610
|
-
request.warning
|
|
20611
|
-
);
|
|
20612
|
-
}
|
|
20613
|
-
if (request.action === "fs") {
|
|
20614
|
-
return confirmationCopy(
|
|
20615
|
-
"Choose filesystem access?",
|
|
20616
|
-
"A napplet requests a browser-mediated filesystem selection.",
|
|
20617
|
-
"Open chooser",
|
|
20618
|
-
`Napplet window: ${request.windowId}`,
|
|
20619
|
-
`Selection: ${request.kind}`,
|
|
20620
|
-
`Purpose: ${request.description}`,
|
|
20621
|
-
"The browser will show its file or directory chooser next."
|
|
20622
|
-
);
|
|
20623
|
-
}
|
|
20624
|
-
if (!("event" in request)) throw new Error(`Unsupported confirmation action: ${request.action}`);
|
|
20625
|
-
const event = request.event;
|
|
20626
|
-
const kind = typeof event.kind === "number" ? event.kind : "unknown";
|
|
20627
|
-
const content = typeof event.content === "string" && event.content.length > 0 ? event.content.slice(0, 240) : "(empty content)";
|
|
20628
|
-
return {
|
|
20629
|
-
title: request.action === "sign" ? "Sign this Nostr event?" : "Publish this Nostr event?",
|
|
20630
|
-
summary: request.action === "sign" ? "The active signer will authorize this event." : "Paja will send this event to the configured relay set.",
|
|
20631
|
-
details: `Kind: ${kind}
|
|
20632
|
-
Content: ${content}`,
|
|
20633
|
-
approveLabel: request.action === "sign" ? "Sign" : "Publish"
|
|
20634
|
-
};
|
|
20635
|
-
}
|
|
20636
|
-
function recordPajaConfirmation(state, request, allowed2) {
|
|
20637
|
-
if (request.action === "upload") {
|
|
20638
|
-
appendPajaMessageLog(state, "paja", {
|
|
20639
|
-
type: `paja.upload.${allowed2 ? "confirmed" : "denied"}`,
|
|
20640
|
-
windowId: request.windowId,
|
|
20641
|
-
dTag: request.napplet.dTag,
|
|
20642
|
-
aggregateHash: request.napplet.aggregateHash,
|
|
20643
|
-
filename: request.filename ?? "(unnamed blob)",
|
|
20644
|
-
size: request.size,
|
|
20645
|
-
mimeType: request.mimeType ?? "application/octet-stream",
|
|
20646
|
-
server: request.server,
|
|
20647
|
-
warning: request.warning
|
|
20648
|
-
});
|
|
20649
|
-
return;
|
|
20650
|
-
}
|
|
20651
|
-
if (request.action === "link") {
|
|
20652
|
-
appendPajaMessageLog(state, "paja", {
|
|
20653
|
-
type: `paja.link.${allowed2 ? "confirmed" : "denied"}`,
|
|
20654
|
-
windowId: request.windowId,
|
|
20655
|
-
dTag: request.napplet.dTag,
|
|
20656
|
-
aggregateHash: request.napplet.aggregateHash,
|
|
20657
|
-
url: request.url
|
|
20658
|
-
});
|
|
20659
|
-
return;
|
|
20660
|
-
}
|
|
20661
|
-
if (request.action === "serial" || request.action === "ble") {
|
|
20662
|
-
appendPajaMessageLog(state, "paja", {
|
|
20663
|
-
type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
|
|
20664
|
-
windowId: request.windowId,
|
|
20665
|
-
label: request.label
|
|
20666
|
-
});
|
|
20667
|
-
return;
|
|
20668
|
-
}
|
|
20669
|
-
if (request.action === "notify") {
|
|
20670
|
-
appendPajaMessageLog(state, "paja", {
|
|
20671
|
-
type: `paja.notify.${allowed2 ? "confirmed" : "denied"}`,
|
|
20672
|
-
windowId: request.windowId,
|
|
20673
|
-
dTag: request.napplet.dTag,
|
|
20674
|
-
aggregateHash: request.napplet.aggregateHash,
|
|
20675
|
-
channel: request.channel
|
|
20676
|
-
});
|
|
20677
|
-
return;
|
|
20678
|
-
}
|
|
20679
|
-
if (request.action === "webrtc") {
|
|
20680
|
-
appendPajaMessageLog(state, "paja", {
|
|
20681
|
-
type: `paja.webrtc.${allowed2 ? "confirmed" : "denied"}`,
|
|
20682
|
-
windowId: request.windowId,
|
|
20683
|
-
dTag: request.napplet.dTag,
|
|
20684
|
-
aggregateHash: request.napplet.aggregateHash
|
|
20685
|
-
});
|
|
20686
|
-
return;
|
|
20687
|
-
}
|
|
20688
|
-
if (request.action === "dm") {
|
|
20689
|
-
appendPajaMessageLog(state, "paja", {
|
|
20690
|
-
type: `paja.dm.${allowed2 ? "confirmed" : "denied"}`,
|
|
20691
|
-
recipients: request.recipients
|
|
20692
|
-
});
|
|
20693
|
-
return;
|
|
20694
|
-
}
|
|
20695
|
-
if (request.action === "fs") {
|
|
20696
|
-
appendPajaMessageLog(state, "paja", {
|
|
20697
|
-
type: `paja.fs.${allowed2 ? "confirmed" : "denied"}`,
|
|
20698
|
-
windowId: request.windowId,
|
|
20699
|
-
kind: request.kind
|
|
20700
|
-
});
|
|
20701
|
-
return;
|
|
20702
|
-
}
|
|
20703
|
-
if (!("event" in request)) return;
|
|
20704
|
-
const event = request.event;
|
|
20705
|
-
appendPajaMessageLog(state, "paja", {
|
|
20706
|
-
type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
|
|
20707
|
-
kind: typeof event.kind === "number" ? event.kind : "unknown"
|
|
20708
|
-
});
|
|
20709
|
-
}
|
|
20710
|
-
function createHostSignerController(getState, refreshState, confirmRequest) {
|
|
20711
|
-
return createPajaSignerController({
|
|
20712
|
-
confirmRequest,
|
|
20713
|
-
onChange(signer) {
|
|
20714
|
-
const state = getState();
|
|
20715
|
-
if (!state) return;
|
|
20716
|
-
state.signer = signer;
|
|
20717
|
-
appendPajaMessageLog(state, "paja", {
|
|
20718
|
-
type: `paja.signer.${signer.method}.${signer.status}`,
|
|
20719
|
-
pubkey: signer.pubkey,
|
|
20720
|
-
relay: signer.relay,
|
|
20721
|
-
error: signer.error
|
|
20722
|
-
});
|
|
20723
|
-
refreshState(state);
|
|
20724
|
-
if (signer.status === "connected") state.reload();
|
|
20725
|
-
}
|
|
20726
|
-
});
|
|
20727
|
-
}
|
|
20728
|
-
function hasNip07Signer() {
|
|
20729
|
-
const signer = globalThis.nostr;
|
|
20730
|
-
return typeof signer === "object" && signer !== null;
|
|
20731
|
-
}
|
|
20732
|
-
|
|
20733
|
-
// src/browser-host-runtime.ts
|
|
20734
|
-
function unregisterSingleFrameWindow(bridge, runtime, windowId) {
|
|
20735
|
-
if (!windowId) return;
|
|
20736
|
-
bridge.runtime.destroyWindow(windowId);
|
|
20737
|
-
bridge.runtime.sessionRegistry.unregister(windowId);
|
|
20738
|
-
originRegistry.unregister(windowId);
|
|
20739
|
-
runtime.readyWindowIds.delete(windowId);
|
|
20740
|
-
if (runtime.currentWindowId === windowId) runtime.currentWindowId = null;
|
|
20741
|
-
}
|
|
20742
|
-
|
|
20743
|
-
// src/browser-runtime-tabs.ts
|
|
20744
|
-
var PAJA_RUNTIME_TABS_STORAGE_KEY = "kehto:paja:runtime-tabs:v1";
|
|
20745
|
-
function resolvedTargetKey(target) {
|
|
20746
|
-
return [
|
|
20747
|
-
target.event.kind,
|
|
20748
|
-
target.event.pubkey,
|
|
20749
|
-
target.dTag,
|
|
20750
|
-
target.aggregateHash
|
|
20751
|
-
].join(":");
|
|
20752
|
-
}
|
|
20753
|
-
function runtimeTabGenerationId(tab) {
|
|
20754
|
-
return `${tab.id}:${tab.generation}`;
|
|
20755
|
-
}
|
|
20756
|
-
function getActiveTab(state) {
|
|
20757
|
-
return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null;
|
|
20758
|
-
}
|
|
20759
|
-
function setEmptyStageVisible(visible) {
|
|
20760
|
-
const empty = document.getElementById("empty-runtime-stage");
|
|
20761
|
-
if (empty instanceof HTMLElement) empty.hidden = !visible;
|
|
20762
|
-
}
|
|
20763
|
-
function createPajaShareUrl(pointerValue, href = window.location.href) {
|
|
20764
|
-
const pointer = pointerValue.trim();
|
|
20765
|
-
const url = new URL(href);
|
|
20766
|
-
url.search = "";
|
|
20767
|
-
url.hash = "";
|
|
20768
|
-
url.searchParams.set(getPointerParamName(pointer), pointer);
|
|
20769
|
-
return url.href;
|
|
20770
|
-
}
|
|
20771
|
-
function snapshotRuntimeTabs(state) {
|
|
20772
|
-
const pointers = state.tabs.map((tab) => tab.pointerValue.trim()).filter((pointer) => pointer.length > 0);
|
|
20773
|
-
if (pointers.length === 0) return null;
|
|
20774
|
-
const activeIndex = Math.max(
|
|
20775
|
-
0,
|
|
20776
|
-
state.tabs.findIndex((tab) => tab.id === state.activeTabId)
|
|
20777
|
-
);
|
|
20778
|
-
return {
|
|
20779
|
-
version: 1,
|
|
20780
|
-
pointers,
|
|
20781
|
-
activeIndex: Math.min(activeIndex, pointers.length - 1)
|
|
20782
|
-
};
|
|
20783
|
-
}
|
|
20784
|
-
function parseRuntimeTabsSnapshot(value) {
|
|
20785
|
-
if (!value) return null;
|
|
20786
|
-
let parsed;
|
|
20787
|
-
try {
|
|
20788
|
-
parsed = JSON.parse(value);
|
|
20789
|
-
} catch {
|
|
20790
|
-
return null;
|
|
21251
|
+
"Allow notifications",
|
|
21252
|
+
`Napplet: ${request.napplet.dTag} (${request.windowId})`,
|
|
21253
|
+
`Identity: ${request.napplet.aggregateHash}`,
|
|
21254
|
+
`Channel: ${request.channel ?? "all notifications"}`
|
|
21255
|
+
);
|
|
20791
21256
|
}
|
|
20792
|
-
if (
|
|
20793
|
-
|
|
20794
|
-
|
|
20795
|
-
|
|
20796
|
-
|
|
20797
|
-
|
|
21257
|
+
if (request.action === "webrtc") {
|
|
21258
|
+
return confirmationCopy(
|
|
21259
|
+
"Start a peer connection?",
|
|
21260
|
+
`${request.napplet.dTag} requests a WebRTC data session.`,
|
|
21261
|
+
"Connect",
|
|
21262
|
+
`Napplet: ${request.napplet.dTag} (${request.windowId})`,
|
|
21263
|
+
`Scope: ${request.scope}`,
|
|
21264
|
+
request.warning
|
|
21265
|
+
);
|
|
21266
|
+
}
|
|
21267
|
+
if (request.action === "dm") {
|
|
21268
|
+
return confirmationCopy(
|
|
21269
|
+
"Send this encrypted message?",
|
|
21270
|
+
"A napplet requests NIP-17 direct-message delivery.",
|
|
21271
|
+
"Send message",
|
|
21272
|
+
`Recipients: ${request.recipients.join(", ")}`,
|
|
21273
|
+
`Content: ${request.content.slice(0, 240)}`,
|
|
21274
|
+
request.warning
|
|
21275
|
+
);
|
|
21276
|
+
}
|
|
21277
|
+
if (request.action === "fs") {
|
|
21278
|
+
return confirmationCopy(
|
|
21279
|
+
"Choose filesystem access?",
|
|
21280
|
+
"A napplet requests a browser-mediated filesystem selection.",
|
|
21281
|
+
"Open chooser",
|
|
21282
|
+
`Napplet window: ${request.windowId}`,
|
|
21283
|
+
`Selection: ${request.kind}`,
|
|
21284
|
+
`Purpose: ${request.description}`,
|
|
21285
|
+
"The browser will show its file or directory chooser next."
|
|
21286
|
+
);
|
|
21287
|
+
}
|
|
21288
|
+
if (!("event" in request)) throw new Error(`Unsupported confirmation action: ${request.action}`);
|
|
21289
|
+
const event = request.event;
|
|
21290
|
+
const kind = typeof event.kind === "number" ? event.kind : "unknown";
|
|
21291
|
+
const content = typeof event.content === "string" && event.content.length > 0 ? event.content.slice(0, 240) : "(empty content)";
|
|
21292
|
+
const signerContext = request.signerContext;
|
|
20798
21293
|
return {
|
|
20799
|
-
|
|
20800
|
-
|
|
20801
|
-
|
|
21294
|
+
title: request.action === "sign" ? "Sign this Nostr event?" : "Publish this Nostr event?",
|
|
21295
|
+
summary: request.action === "sign" ? signerContext ? `${signerContext.napplet.dTag} requests a signature from the active signer.` : "A Paja host operation requests a signature from the active signer." : "Paja will send this event to the configured relay set.",
|
|
21296
|
+
details: confirmationDetails(
|
|
21297
|
+
signerContext ? `Napplet: ${signerContext.napplet.dTag} (${signerContext.windowId})` : void 0,
|
|
21298
|
+
signerContext ? `Identity hash: ${signerContext.napplet.aggregateHash}` : void 0,
|
|
21299
|
+
signerContext?.runtimeScope.startsWith("target-url:") ? `Target URL: ${signerContext.runtimeScope.slice("target-url:".length)}` : void 0,
|
|
21300
|
+
signerContext ? `Signer: ${signerContext.signerPubkey}` : void 0,
|
|
21301
|
+
`Kind: ${kind}`,
|
|
21302
|
+
`Content: ${content}`
|
|
21303
|
+
),
|
|
21304
|
+
approveLabel: request.action === "sign" ? "Sign" : "Publish"
|
|
20802
21305
|
};
|
|
20803
21306
|
}
|
|
20804
|
-
function
|
|
20805
|
-
|
|
20806
|
-
|
|
20807
|
-
|
|
20808
|
-
|
|
20809
|
-
|
|
20810
|
-
|
|
20811
|
-
|
|
20812
|
-
|
|
20813
|
-
|
|
20814
|
-
|
|
20815
|
-
|
|
20816
|
-
|
|
20817
|
-
const input = document.getElementById("runtime-pointer-input");
|
|
20818
|
-
if (input instanceof HTMLInputElement) input.value = tab.pointerValue;
|
|
20819
|
-
context.setPointerStatus(state, tab.pointerStatus);
|
|
20820
|
-
context.setStatus(state, tab.status);
|
|
20821
|
-
setEmptyStageVisible(false);
|
|
20822
|
-
for (const entry of state.tabs) entry.frame.hidden = entry.id !== tab.id;
|
|
20823
|
-
renderRuntimeTabs(state);
|
|
20824
|
-
renderPajaDevtools(state, { bridge: context.bridge, devSignerPubkey: PAJA_DEV_SIGNER_PUBKEY });
|
|
20825
|
-
}
|
|
20826
|
-
function closeRuntimeTab(state, context, tabId) {
|
|
20827
|
-
const index = state.tabs.findIndex((tab2) => tab2.id === tabId);
|
|
20828
|
-
if (index < 0) return;
|
|
20829
|
-
const [tab] = state.tabs.splice(index, 1);
|
|
20830
|
-
destroyRuntimeTab(tab, context);
|
|
20831
|
-
if (state.activeTabId !== tabId) {
|
|
20832
|
-
renderRuntimeTabs(state);
|
|
20833
|
-
setEmptyStageVisible(state.tabs.length === 0);
|
|
21307
|
+
function recordPajaConfirmation(state, request, allowed2, signerApprovalMode) {
|
|
21308
|
+
if (request.action === "upload") {
|
|
21309
|
+
appendPajaMessageLog(state, "paja", {
|
|
21310
|
+
type: `paja.upload.${allowed2 ? "confirmed" : "denied"}`,
|
|
21311
|
+
windowId: request.windowId,
|
|
21312
|
+
dTag: request.napplet.dTag,
|
|
21313
|
+
aggregateHash: request.napplet.aggregateHash,
|
|
21314
|
+
filename: request.filename ?? "(unnamed blob)",
|
|
21315
|
+
size: request.size,
|
|
21316
|
+
mimeType: request.mimeType ?? "application/octet-stream",
|
|
21317
|
+
server: request.server,
|
|
21318
|
+
warning: request.warning
|
|
21319
|
+
});
|
|
20834
21320
|
return;
|
|
20835
21321
|
}
|
|
20836
|
-
|
|
20837
|
-
|
|
20838
|
-
|
|
21322
|
+
if (request.action === "link") {
|
|
21323
|
+
appendPajaMessageLog(state, "paja", {
|
|
21324
|
+
type: `paja.link.${allowed2 ? "confirmed" : "denied"}`,
|
|
21325
|
+
windowId: request.windowId,
|
|
21326
|
+
dTag: request.napplet.dTag,
|
|
21327
|
+
aggregateHash: request.napplet.aggregateHash,
|
|
21328
|
+
url: request.url
|
|
21329
|
+
});
|
|
20839
21330
|
return;
|
|
20840
21331
|
}
|
|
20841
|
-
|
|
20842
|
-
|
|
20843
|
-
|
|
20844
|
-
|
|
20845
|
-
|
|
20846
|
-
|
|
20847
|
-
|
|
20848
|
-
renderRuntimeTabs(state);
|
|
20849
|
-
}
|
|
20850
|
-
function addRuntimeTab(state, context, pointerValue, resolvedTarget) {
|
|
20851
|
-
state.generation += 1;
|
|
20852
|
-
const tab = {
|
|
20853
|
-
id: `tab-${state.generation}`,
|
|
20854
|
-
title: resolvedTargetTitle(resolvedTarget),
|
|
20855
|
-
key: resolvedTargetKey(resolvedTarget),
|
|
20856
|
-
pointerValue,
|
|
20857
|
-
pointerStatus: `${resolvedTarget.dTag}:${resolvedTarget.aggregateHash.slice(0, 12)}`,
|
|
20858
|
-
resolvedTarget,
|
|
20859
|
-
frame: document.createElement("iframe"),
|
|
20860
|
-
generation: state.generation,
|
|
20861
|
-
windowId: null,
|
|
20862
|
-
status: "booting"
|
|
20863
|
-
};
|
|
20864
|
-
tab.frame = createRuntimeTabFrame(tab, state, context);
|
|
20865
|
-
context.stage.append(tab.frame);
|
|
20866
|
-
state.tabs.push(tab);
|
|
20867
|
-
activateRuntimeTab(state, context, tab.id);
|
|
20868
|
-
startRuntimeTabNavigation(tab, state, context);
|
|
20869
|
-
return tab;
|
|
20870
|
-
}
|
|
20871
|
-
function reloadActiveRuntimeTab(state, context) {
|
|
20872
|
-
const tab = getActiveTab(state);
|
|
20873
|
-
if (!tab) return;
|
|
20874
|
-
destroyRuntimeTab(tab, context);
|
|
20875
|
-
tab.generation = ++state.generation;
|
|
20876
|
-
tab.windowId = null;
|
|
20877
|
-
tab.status = "reloading";
|
|
20878
|
-
tab.frame = createRuntimeTabFrame(tab, state, context);
|
|
20879
|
-
context.stage.append(tab.frame);
|
|
20880
|
-
activateRuntimeTab(state, context, tab.id);
|
|
20881
|
-
startRuntimeTabNavigation(tab, state, context);
|
|
20882
|
-
}
|
|
20883
|
-
function showDuplicatePointerDialog() {
|
|
20884
|
-
const backdrop = document.getElementById("duplicate-pointer-dialog");
|
|
20885
|
-
const loadAgain = document.getElementById("duplicate-load-again");
|
|
20886
|
-
const openTab = document.getElementById("duplicate-open-tab");
|
|
20887
|
-
const cancel = document.getElementById("duplicate-cancel");
|
|
20888
|
-
if (!(backdrop instanceof HTMLElement) || !(loadAgain instanceof HTMLButtonElement) || !(openTab instanceof HTMLButtonElement) || !(cancel instanceof HTMLButtonElement)) {
|
|
20889
|
-
return Promise.resolve("cancel");
|
|
21332
|
+
if (request.action === "serial" || request.action === "ble") {
|
|
21333
|
+
appendPajaMessageLog(state, "paja", {
|
|
21334
|
+
type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
|
|
21335
|
+
windowId: request.windowId,
|
|
21336
|
+
label: request.label
|
|
21337
|
+
});
|
|
21338
|
+
return;
|
|
20890
21339
|
}
|
|
20891
|
-
|
|
20892
|
-
|
|
20893
|
-
|
|
20894
|
-
|
|
20895
|
-
|
|
20896
|
-
|
|
20897
|
-
|
|
20898
|
-
|
|
20899
|
-
|
|
20900
|
-
|
|
20901
|
-
|
|
20902
|
-
|
|
20903
|
-
|
|
20904
|
-
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
cancel.addEventListener("click", onCancel);
|
|
20909
|
-
backdrop.addEventListener("keydown", onKeydown);
|
|
20910
|
-
backdrop.hidden = false;
|
|
20911
|
-
loadAgain.focus();
|
|
20912
|
-
});
|
|
20913
|
-
}
|
|
20914
|
-
function renderTab(state, tab) {
|
|
20915
|
-
const tabButton = document.createElement("div");
|
|
20916
|
-
tabButton.className = "tab";
|
|
20917
|
-
tabButton.dataset.tabId = tab.id;
|
|
20918
|
-
tabButton.dataset.active = String(tab.id === state.activeTabId);
|
|
20919
|
-
tabButton.setAttribute("role", "tab");
|
|
20920
|
-
tabButton.setAttribute("aria-selected", String(tab.id === state.activeTabId));
|
|
20921
|
-
tabButton.tabIndex = 0;
|
|
20922
|
-
tabButton.title = tab.pointerValue;
|
|
20923
|
-
tabButton.addEventListener("click", () => state.activateTab(tab.id));
|
|
20924
|
-
tabButton.addEventListener("keydown", (event) => {
|
|
20925
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
20926
|
-
event.preventDefault();
|
|
20927
|
-
state.activateTab(tab.id);
|
|
20928
|
-
});
|
|
20929
|
-
const label = document.createElement("span");
|
|
20930
|
-
label.className = "tab-label";
|
|
20931
|
-
label.textContent = tab.title;
|
|
20932
|
-
tabButton.append(label, renderShareButton(tab), renderCloseButton(state, tab));
|
|
20933
|
-
return tabButton;
|
|
20934
|
-
}
|
|
20935
|
-
function renderShareButton(tab) {
|
|
20936
|
-
const share = document.createElement("button");
|
|
20937
|
-
share.type = "button";
|
|
20938
|
-
share.className = "tab-share";
|
|
20939
|
-
share.setAttribute("aria-label", `Copy share link for ${tab.title}`);
|
|
20940
|
-
share.title = "Copy share link";
|
|
20941
|
-
share.textContent = "\u2197";
|
|
20942
|
-
share.addEventListener("click", (event) => {
|
|
20943
|
-
event.stopPropagation();
|
|
20944
|
-
void shareRuntimeTab(tab, share).catch((error) => console.error(error));
|
|
20945
|
-
});
|
|
20946
|
-
share.addEventListener("keydown", (event) => {
|
|
20947
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
20948
|
-
event.preventDefault();
|
|
20949
|
-
event.stopPropagation();
|
|
20950
|
-
void shareRuntimeTab(tab, share).catch((error) => console.error(error));
|
|
20951
|
-
});
|
|
20952
|
-
return share;
|
|
20953
|
-
}
|
|
20954
|
-
function renderCloseButton(state, tab) {
|
|
20955
|
-
const close = document.createElement("button");
|
|
20956
|
-
close.type = "button";
|
|
20957
|
-
close.className = "tab-close";
|
|
20958
|
-
close.setAttribute("aria-label", `Close ${tab.title}`);
|
|
20959
|
-
close.textContent = "x";
|
|
20960
|
-
close.addEventListener("click", (event) => {
|
|
20961
|
-
event.stopPropagation();
|
|
20962
|
-
state.closeTab(tab.id);
|
|
20963
|
-
});
|
|
20964
|
-
close.addEventListener("keydown", (event) => {
|
|
20965
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
20966
|
-
event.preventDefault();
|
|
20967
|
-
event.stopPropagation();
|
|
20968
|
-
state.closeTab(tab.id);
|
|
20969
|
-
});
|
|
20970
|
-
return close;
|
|
20971
|
-
}
|
|
20972
|
-
async function shareRuntimeTab(tab, button2) {
|
|
20973
|
-
const shareUrl = createPajaShareUrl(tab.pointerValue);
|
|
20974
|
-
const clipboard = navigator.clipboard;
|
|
20975
|
-
if (!clipboard) {
|
|
20976
|
-
window.prompt("Copy Paja share link", shareUrl);
|
|
21340
|
+
if (request.action === "notify") {
|
|
21341
|
+
appendPajaMessageLog(state, "paja", {
|
|
21342
|
+
type: `paja.notify.${allowed2 ? "confirmed" : "denied"}`,
|
|
21343
|
+
windowId: request.windowId,
|
|
21344
|
+
dTag: request.napplet.dTag,
|
|
21345
|
+
aggregateHash: request.napplet.aggregateHash,
|
|
21346
|
+
channel: request.channel
|
|
21347
|
+
});
|
|
21348
|
+
return;
|
|
21349
|
+
}
|
|
21350
|
+
if (request.action === "webrtc") {
|
|
21351
|
+
appendPajaMessageLog(state, "paja", {
|
|
21352
|
+
type: `paja.webrtc.${allowed2 ? "confirmed" : "denied"}`,
|
|
21353
|
+
windowId: request.windowId,
|
|
21354
|
+
dTag: request.napplet.dTag,
|
|
21355
|
+
aggregateHash: request.napplet.aggregateHash
|
|
21356
|
+
});
|
|
20977
21357
|
return;
|
|
20978
21358
|
}
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
|
|
21359
|
+
if (request.action === "dm") {
|
|
21360
|
+
appendPajaMessageLog(state, "paja", {
|
|
21361
|
+
type: `paja.dm.${allowed2 ? "confirmed" : "denied"}`,
|
|
21362
|
+
recipients: request.recipients
|
|
21363
|
+
});
|
|
20983
21364
|
return;
|
|
20984
21365
|
}
|
|
20985
|
-
|
|
20986
|
-
|
|
20987
|
-
|
|
20988
|
-
|
|
20989
|
-
|
|
20990
|
-
|
|
20991
|
-
|
|
20992
|
-
if (pointer.startsWith("nevent")) return "nevent";
|
|
20993
|
-
return "pointer";
|
|
20994
|
-
}
|
|
20995
|
-
function resolvedTargetTitle(target) {
|
|
20996
|
-
const titleTag = target.event.tags.find((tag) => tag[0] === "title")?.[1];
|
|
20997
|
-
const title = typeof target.manifest.title === "string" && target.manifest.title.trim().length > 0 ? target.manifest.title.trim() : titleTag?.trim();
|
|
20998
|
-
return title && title.length > 0 ? title : target.dTag || target.aggregateHash.slice(0, 12);
|
|
20999
|
-
}
|
|
21000
|
-
function destroyRuntimeTab(tab, context) {
|
|
21001
|
-
context.onTabDestroyed?.(tab);
|
|
21002
|
-
if (tab.windowId) {
|
|
21003
|
-
context.bridge.runtime.destroyWindow(tab.windowId);
|
|
21004
|
-
context.bridge.runtime.sessionRegistry.unregister(tab.windowId);
|
|
21005
|
-
originRegistry.unregister(tab.windowId);
|
|
21006
|
-
context.runtime.readyWindowIds.delete(tab.windowId);
|
|
21007
|
-
if (context.runtime.currentWindowId === tab.windowId) context.runtime.currentWindowId = null;
|
|
21366
|
+
if (request.action === "fs") {
|
|
21367
|
+
appendPajaMessageLog(state, "paja", {
|
|
21368
|
+
type: `paja.fs.${allowed2 ? "confirmed" : "denied"}`,
|
|
21369
|
+
windowId: request.windowId,
|
|
21370
|
+
kind: request.kind
|
|
21371
|
+
});
|
|
21372
|
+
return;
|
|
21008
21373
|
}
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
|
|
21013
|
-
|
|
21014
|
-
|
|
21015
|
-
|
|
21016
|
-
|
|
21017
|
-
|
|
21018
|
-
|
|
21019
|
-
|
|
21020
|
-
|
|
21021
|
-
frame.addEventListener("error", () => {
|
|
21022
|
-
tab.status = "error";
|
|
21023
|
-
if (state.activeTabId === tab.id) context.setStatus(state, "error");
|
|
21024
|
-
renderRuntimeTabs(state);
|
|
21374
|
+
if (!("event" in request)) return;
|
|
21375
|
+
const event = request.event;
|
|
21376
|
+
const signerContext = request.signerContext;
|
|
21377
|
+
appendPajaMessageLog(state, "paja", {
|
|
21378
|
+
type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
|
|
21379
|
+
kind: typeof event.kind === "number" ? event.kind : "unknown",
|
|
21380
|
+
windowId: signerContext?.windowId,
|
|
21381
|
+
dTag: signerContext?.napplet.dTag,
|
|
21382
|
+
aggregateHash: signerContext?.napplet.aggregateHash,
|
|
21383
|
+
runtimeScope: signerContext?.runtimeScope,
|
|
21384
|
+
signerPubkey: signerContext?.signerPubkey,
|
|
21385
|
+
approval: signerApprovalMode
|
|
21025
21386
|
});
|
|
21026
|
-
return frame;
|
|
21027
21387
|
}
|
|
21028
|
-
function
|
|
21029
|
-
|
|
21030
|
-
|
|
21031
|
-
|
|
21032
|
-
|
|
21033
|
-
|
|
21034
|
-
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
|
|
21038
|
-
|
|
21039
|
-
|
|
21040
|
-
|
|
21041
|
-
|
|
21042
|
-
if (
|
|
21043
|
-
tab.windowId = windowId;
|
|
21044
|
-
if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
|
|
21388
|
+
function createHostSignerController(getState, refreshState, confirmRequest) {
|
|
21389
|
+
return createPajaSignerController({
|
|
21390
|
+
confirmRequest,
|
|
21391
|
+
onChange(signer) {
|
|
21392
|
+
const state = getState();
|
|
21393
|
+
if (!state) return;
|
|
21394
|
+
state.signer = signer;
|
|
21395
|
+
appendPajaMessageLog(state, "paja", {
|
|
21396
|
+
type: `paja.signer.${signer.method}.${signer.status}`,
|
|
21397
|
+
pubkey: signer.pubkey,
|
|
21398
|
+
relay: signer.relay,
|
|
21399
|
+
error: signer.error
|
|
21400
|
+
});
|
|
21401
|
+
refreshState(state);
|
|
21402
|
+
if (signer.status === "connected") state.reload();
|
|
21045
21403
|
}
|
|
21046
|
-
).then((windowId) => {
|
|
21047
|
-
if (tab.generation !== generation) return;
|
|
21048
|
-
tab.windowId = windowId;
|
|
21049
|
-
if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
|
|
21050
|
-
}).catch((error) => {
|
|
21051
|
-
if (tab.generation !== generation) return;
|
|
21052
|
-
tab.frame.removeAttribute("src");
|
|
21053
|
-
tab.frame.srcdoc = context.renderTargetErrorHtml(error);
|
|
21054
|
-
tab.status = "error";
|
|
21055
|
-
if (state.activeTabId === tab.id) context.setStatus(state, "error");
|
|
21056
|
-
appendPajaMessageLog(state, "paja", {
|
|
21057
|
-
type: "paja.target.error",
|
|
21058
|
-
error: error instanceof Error ? error.message : String(error)
|
|
21059
|
-
}, tab.windowId ?? void 0);
|
|
21060
|
-
renderRuntimeTabs(state);
|
|
21061
|
-
console.error(error);
|
|
21062
21404
|
});
|
|
21063
21405
|
}
|
|
21064
|
-
function
|
|
21065
|
-
|
|
21406
|
+
function hasNip07Signer() {
|
|
21407
|
+
const signer = globalThis.nostr;
|
|
21408
|
+
return typeof signer === "object" && signer !== null;
|
|
21066
21409
|
}
|
|
21067
21410
|
|
|
21068
|
-
// src/runtime
|
|
21069
|
-
|
|
21070
|
-
|
|
21071
|
-
|
|
21072
|
-
|
|
21073
|
-
|
|
21074
|
-
|
|
21075
|
-
|
|
21076
|
-
value: trimmed,
|
|
21077
|
-
identifier: decoded.data.identifier,
|
|
21078
|
-
pubkey: decoded.data.pubkey,
|
|
21079
|
-
kind: decoded.data.kind,
|
|
21080
|
-
relays: [...decoded.data.relays ?? []]
|
|
21081
|
-
};
|
|
21082
|
-
}
|
|
21083
|
-
if (decoded.type === "nevent") {
|
|
21084
|
-
return {
|
|
21085
|
-
type: "nevent",
|
|
21086
|
-
value: trimmed,
|
|
21087
|
-
id: decoded.data.id,
|
|
21088
|
-
...decoded.data.author ? { author: decoded.data.author } : {},
|
|
21089
|
-
...decoded.data.kind ? { kind: decoded.data.kind } : {},
|
|
21090
|
-
relays: [...decoded.data.relays ?? []]
|
|
21091
|
-
};
|
|
21092
|
-
}
|
|
21093
|
-
throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
|
|
21411
|
+
// src/browser-host-runtime.ts
|
|
21412
|
+
function unregisterSingleFrameWindow(bridge, runtime, windowId) {
|
|
21413
|
+
if (!windowId) return;
|
|
21414
|
+
bridge.runtime.destroyWindow(windowId);
|
|
21415
|
+
bridge.runtime.sessionRegistry.unregister(windowId);
|
|
21416
|
+
originRegistry.unregister(windowId);
|
|
21417
|
+
runtime.readyWindowIds.delete(windowId);
|
|
21418
|
+
if (runtime.currentWindowId === windowId) runtime.currentWindowId = null;
|
|
21094
21419
|
}
|
|
21095
|
-
|
|
21096
|
-
|
|
21097
|
-
|
|
21098
|
-
|
|
21099
|
-
|
|
21100
|
-
|
|
21101
|
-
|
|
21102
|
-
|
|
21103
|
-
|
|
21104
|
-
|
|
21105
|
-
const blossomServers = [...options.blossomServers ?? []];
|
|
21106
|
-
const resolved = await resolveNapplet({
|
|
21107
|
-
event,
|
|
21108
|
-
cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
|
|
21109
|
-
fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
|
|
21110
|
-
const response = await (options.fetcher ?? fetch)(url);
|
|
21111
|
-
if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
|
|
21112
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
21113
|
-
})
|
|
21114
|
-
});
|
|
21115
|
-
return {
|
|
21116
|
-
pointer,
|
|
21117
|
-
event,
|
|
21118
|
-
relays,
|
|
21119
|
-
blossomServers,
|
|
21120
|
-
dTag: resolved.dTag,
|
|
21121
|
-
aggregateHash: resolved.aggregateHash,
|
|
21122
|
-
indexHtml: resolved.indexHtml,
|
|
21123
|
-
manifest: resolved.manifest
|
|
21124
|
-
};
|
|
21125
|
-
} finally {
|
|
21126
|
-
if (ownsPool) {
|
|
21127
|
-
pool.close?.(relays);
|
|
21128
|
-
pool.destroy?.();
|
|
21129
|
-
}
|
|
21130
|
-
}
|
|
21420
|
+
|
|
21421
|
+
// src/browser-runtime-tabs.ts
|
|
21422
|
+
var PAJA_RUNTIME_TABS_STORAGE_KEY = "kehto:paja:runtime-tabs:v1";
|
|
21423
|
+
function resolvedTargetKey(target) {
|
|
21424
|
+
return [
|
|
21425
|
+
target.event.kind,
|
|
21426
|
+
target.event.pubkey,
|
|
21427
|
+
target.dTag,
|
|
21428
|
+
target.aggregateHash
|
|
21429
|
+
].join(":");
|
|
21131
21430
|
}
|
|
21132
|
-
function
|
|
21133
|
-
|
|
21134
|
-
|
|
21135
|
-
|
|
21136
|
-
|
|
21137
|
-
|
|
21138
|
-
|
|
21139
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
21142
|
-
|
|
21143
|
-
|
|
21144
|
-
|
|
21145
|
-
|
|
21146
|
-
|
|
21147
|
-
|
|
21148
|
-
|
|
21149
|
-
|
|
21150
|
-
|
|
21151
|
-
|
|
21152
|
-
|
|
21153
|
-
const
|
|
21154
|
-
|
|
21155
|
-
|
|
21156
|
-
|
|
21157
|
-
|
|
21158
|
-
|
|
21431
|
+
function runtimeTabGenerationId(tab) {
|
|
21432
|
+
return `${tab.id}:${tab.generation}`;
|
|
21433
|
+
}
|
|
21434
|
+
function getActiveTab(state) {
|
|
21435
|
+
return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null;
|
|
21436
|
+
}
|
|
21437
|
+
function setEmptyStageVisible(visible) {
|
|
21438
|
+
const empty = document.getElementById("empty-runtime-stage");
|
|
21439
|
+
if (empty instanceof HTMLElement) empty.hidden = !visible;
|
|
21440
|
+
}
|
|
21441
|
+
function createPajaShareUrl(pointerValue, href = window.location.href) {
|
|
21442
|
+
const pointer = pointerValue.trim();
|
|
21443
|
+
const url = new URL(href);
|
|
21444
|
+
url.search = "";
|
|
21445
|
+
url.hash = "";
|
|
21446
|
+
url.searchParams.set(getPointerParamName(pointer), pointer);
|
|
21447
|
+
return url.href;
|
|
21448
|
+
}
|
|
21449
|
+
function snapshotRuntimeTabs(state) {
|
|
21450
|
+
const pointers = state.tabs.map((tab) => tab.pointerValue.trim()).filter((pointer) => pointer.length > 0);
|
|
21451
|
+
if (pointers.length === 0) return null;
|
|
21452
|
+
const activeIndex = Math.max(
|
|
21453
|
+
0,
|
|
21454
|
+
state.tabs.findIndex((tab) => tab.id === state.activeTabId)
|
|
21455
|
+
);
|
|
21456
|
+
return {
|
|
21457
|
+
version: 1,
|
|
21458
|
+
pointers,
|
|
21459
|
+
activeIndex: Math.min(activeIndex, pointers.length - 1)
|
|
21460
|
+
};
|
|
21461
|
+
}
|
|
21462
|
+
function parseRuntimeTabsSnapshot(value) {
|
|
21463
|
+
if (!value) return null;
|
|
21464
|
+
let parsed;
|
|
21465
|
+
try {
|
|
21466
|
+
parsed = JSON.parse(value);
|
|
21467
|
+
} catch {
|
|
21468
|
+
return null;
|
|
21159
21469
|
}
|
|
21160
|
-
return
|
|
21470
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
21471
|
+
const candidate = parsed;
|
|
21472
|
+
if (candidate.version !== 1 || !Array.isArray(candidate.pointers)) return null;
|
|
21473
|
+
const pointers = candidate.pointers.filter((pointer) => typeof pointer === "string").map((pointer) => pointer.trim()).filter((pointer) => pointer.length > 0);
|
|
21474
|
+
if (pointers.length === 0) return null;
|
|
21475
|
+
const rawActiveIndex = typeof candidate.activeIndex === "number" && Number.isInteger(candidate.activeIndex) ? candidate.activeIndex : 0;
|
|
21476
|
+
return {
|
|
21477
|
+
version: 1,
|
|
21478
|
+
pointers,
|
|
21479
|
+
activeIndex: Math.max(0, Math.min(rawActiveIndex, pointers.length - 1))
|
|
21480
|
+
};
|
|
21161
21481
|
}
|
|
21162
|
-
|
|
21163
|
-
const
|
|
21164
|
-
|
|
21165
|
-
|
|
21166
|
-
|
|
21167
|
-
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
|
|
21171
|
-
|
|
21172
|
-
|
|
21173
|
-
|
|
21174
|
-
|
|
21175
|
-
|
|
21176
|
-
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
|
|
21482
|
+
function renderRuntimeTabs(state) {
|
|
21483
|
+
const tabsEl = document.getElementById("napplet-tabs");
|
|
21484
|
+
if (!(tabsEl instanceof HTMLElement)) return;
|
|
21485
|
+
tabsEl.replaceChildren(...state.tabs.map((tab) => renderTab(state, tab)));
|
|
21486
|
+
}
|
|
21487
|
+
function activateRuntimeTab(state, context, tabId) {
|
|
21488
|
+
const tab = state.tabs.find((entry) => entry.id === tabId);
|
|
21489
|
+
if (!tab) return;
|
|
21490
|
+
state.activeTabId = tab.id;
|
|
21491
|
+
state.resolvedTarget = tab.resolvedTarget;
|
|
21492
|
+
state.pointerValue = tab.pointerValue;
|
|
21493
|
+
state.pointerStatus = tab.pointerStatus;
|
|
21494
|
+
context.runtime.currentWindowId = tab.windowId;
|
|
21495
|
+
const input = document.getElementById("runtime-pointer-input");
|
|
21496
|
+
if (input instanceof HTMLInputElement) input.value = tab.pointerValue;
|
|
21497
|
+
context.setPointerStatus(state, tab.pointerStatus);
|
|
21498
|
+
context.setStatus(state, tab.status);
|
|
21499
|
+
setEmptyStageVisible(false);
|
|
21500
|
+
for (const entry of state.tabs) entry.frame.hidden = entry.id !== tab.id;
|
|
21501
|
+
renderRuntimeTabs(state);
|
|
21502
|
+
renderPajaDevtools(state, { bridge: context.bridge, devSignerPubkey: PAJA_DEV_SIGNER_PUBKEY });
|
|
21503
|
+
}
|
|
21504
|
+
function closeRuntimeTab(state, context, tabId) {
|
|
21505
|
+
const index = state.tabs.findIndex((tab2) => tab2.id === tabId);
|
|
21506
|
+
if (index < 0) return;
|
|
21507
|
+
const [tab] = state.tabs.splice(index, 1);
|
|
21508
|
+
destroyRuntimeTab(tab, context);
|
|
21509
|
+
if (state.activeTabId !== tabId) {
|
|
21510
|
+
renderRuntimeTabs(state);
|
|
21511
|
+
setEmptyStageVisible(state.tabs.length === 0);
|
|
21512
|
+
return;
|
|
21180
21513
|
}
|
|
21181
|
-
|
|
21182
|
-
|
|
21514
|
+
const next = state.tabs[Math.min(index, state.tabs.length - 1)] ?? null;
|
|
21515
|
+
if (next) {
|
|
21516
|
+
activateRuntimeTab(state, context, next.id);
|
|
21517
|
+
return;
|
|
21183
21518
|
}
|
|
21184
|
-
|
|
21519
|
+
state.activeTabId = null;
|
|
21520
|
+
state.resolvedTarget = null;
|
|
21521
|
+
context.runtime.currentWindowId = null;
|
|
21522
|
+
state.pointerStatus = "idle";
|
|
21523
|
+
context.setPointerStatus(state, "idle");
|
|
21524
|
+
context.setStatus(state, "ready");
|
|
21525
|
+
setEmptyStageVisible(true);
|
|
21526
|
+
renderRuntimeTabs(state);
|
|
21185
21527
|
}
|
|
21186
|
-
function
|
|
21187
|
-
|
|
21188
|
-
|
|
21189
|
-
|
|
21190
|
-
|
|
21528
|
+
function addRuntimeTab(state, context, pointerValue, resolvedTarget) {
|
|
21529
|
+
state.generation += 1;
|
|
21530
|
+
const tab = {
|
|
21531
|
+
id: `tab-${state.generation}`,
|
|
21532
|
+
title: resolvedTargetTitle(resolvedTarget),
|
|
21533
|
+
key: resolvedTargetKey(resolvedTarget),
|
|
21534
|
+
pointerValue,
|
|
21535
|
+
pointerStatus: `${resolvedTarget.dTag}:${resolvedTarget.aggregateHash.slice(0, 12)}`,
|
|
21536
|
+
resolvedTarget,
|
|
21537
|
+
frame: document.createElement("iframe"),
|
|
21538
|
+
generation: state.generation,
|
|
21539
|
+
windowId: null,
|
|
21540
|
+
status: "booting"
|
|
21541
|
+
};
|
|
21542
|
+
tab.frame = createRuntimeTabFrame(tab, state, context);
|
|
21543
|
+
context.stage.append(tab.frame);
|
|
21544
|
+
state.tabs.push(tab);
|
|
21545
|
+
activateRuntimeTab(state, context, tab.id);
|
|
21546
|
+
startRuntimeTabNavigation(tab, state, context);
|
|
21547
|
+
return tab;
|
|
21191
21548
|
}
|
|
21192
|
-
function
|
|
21549
|
+
function reloadActiveRuntimeTab(state, context) {
|
|
21550
|
+
const tab = getActiveTab(state);
|
|
21551
|
+
if (!tab) return;
|
|
21552
|
+
destroyRuntimeTab(tab, context);
|
|
21553
|
+
tab.generation = ++state.generation;
|
|
21554
|
+
tab.windowId = null;
|
|
21555
|
+
tab.status = "reloading";
|
|
21556
|
+
tab.frame = createRuntimeTabFrame(tab, state, context);
|
|
21557
|
+
context.stage.append(tab.frame);
|
|
21558
|
+
activateRuntimeTab(state, context, tab.id);
|
|
21559
|
+
startRuntimeTabNavigation(tab, state, context);
|
|
21560
|
+
}
|
|
21561
|
+
function showDuplicatePointerDialog() {
|
|
21562
|
+
const backdrop = document.getElementById("duplicate-pointer-dialog");
|
|
21563
|
+
const loadAgain = document.getElementById("duplicate-load-again");
|
|
21564
|
+
const openTab = document.getElementById("duplicate-open-tab");
|
|
21565
|
+
const cancel = document.getElementById("duplicate-cancel");
|
|
21566
|
+
if (!(backdrop instanceof HTMLElement) || !(loadAgain instanceof HTMLButtonElement) || !(openTab instanceof HTMLButtonElement) || !(cancel instanceof HTMLButtonElement)) {
|
|
21567
|
+
return Promise.resolve("cancel");
|
|
21568
|
+
}
|
|
21193
21569
|
return new Promise((resolve) => {
|
|
21194
|
-
const
|
|
21195
|
-
|
|
21196
|
-
|
|
21197
|
-
|
|
21198
|
-
|
|
21199
|
-
|
|
21200
|
-
|
|
21201
|
-
resolve({ events, ...result2 });
|
|
21570
|
+
const cleanup = (choice) => {
|
|
21571
|
+
backdrop.hidden = true;
|
|
21572
|
+
loadAgain.removeEventListener("click", onLoadAgain);
|
|
21573
|
+
openTab.removeEventListener("click", onOpenTab);
|
|
21574
|
+
cancel.removeEventListener("click", onCancel);
|
|
21575
|
+
backdrop.removeEventListener("keydown", onKeydown);
|
|
21576
|
+
resolve(choice);
|
|
21202
21577
|
};
|
|
21203
|
-
const
|
|
21204
|
-
|
|
21205
|
-
|
|
21206
|
-
|
|
21207
|
-
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
finish({ reasons, timedOut: false });
|
|
21216
|
-
}
|
|
21217
|
-
});
|
|
21218
|
-
} catch (error) {
|
|
21219
|
-
finish({ reasons: [], timedOut: false, error });
|
|
21220
|
-
}
|
|
21578
|
+
const onLoadAgain = () => cleanup("load-again");
|
|
21579
|
+
const onOpenTab = () => cleanup("open-tab");
|
|
21580
|
+
const onCancel = () => cleanup("cancel");
|
|
21581
|
+
const onKeydown = (event) => {
|
|
21582
|
+
if (event.key === "Escape") cleanup("cancel");
|
|
21583
|
+
};
|
|
21584
|
+
loadAgain.addEventListener("click", onLoadAgain);
|
|
21585
|
+
openTab.addEventListener("click", onOpenTab);
|
|
21586
|
+
cancel.addEventListener("click", onCancel);
|
|
21587
|
+
backdrop.addEventListener("keydown", onKeydown);
|
|
21588
|
+
backdrop.hidden = false;
|
|
21589
|
+
loadAgain.focus();
|
|
21221
21590
|
});
|
|
21222
21591
|
}
|
|
21223
|
-
function
|
|
21224
|
-
|
|
21225
|
-
|
|
21226
|
-
|
|
21227
|
-
|
|
21228
|
-
|
|
21229
|
-
|
|
21230
|
-
|
|
21231
|
-
|
|
21232
|
-
|
|
21233
|
-
|
|
21234
|
-
|
|
21235
|
-
|
|
21236
|
-
|
|
21237
|
-
label: "kehto-paja-runtime"
|
|
21238
|
-
}).then(
|
|
21239
|
-
(events) => finish({ events, reasons: [], timedOut: false }),
|
|
21240
|
-
(error) => finish({ events: [], reasons: [], timedOut: false, error })
|
|
21241
|
-
);
|
|
21592
|
+
function renderTab(state, tab) {
|
|
21593
|
+
const tabButton = document.createElement("div");
|
|
21594
|
+
tabButton.className = "tab";
|
|
21595
|
+
tabButton.dataset.tabId = tab.id;
|
|
21596
|
+
tabButton.dataset.active = String(tab.id === state.activeTabId);
|
|
21597
|
+
tabButton.setAttribute("role", "tab");
|
|
21598
|
+
tabButton.setAttribute("aria-selected", String(tab.id === state.activeTabId));
|
|
21599
|
+
tabButton.tabIndex = 0;
|
|
21600
|
+
tabButton.title = tab.pointerValue;
|
|
21601
|
+
tabButton.addEventListener("click", () => state.activateTab(tab.id));
|
|
21602
|
+
tabButton.addEventListener("keydown", (event) => {
|
|
21603
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
21604
|
+
event.preventDefault();
|
|
21605
|
+
state.activateTab(tab.id);
|
|
21242
21606
|
});
|
|
21607
|
+
const label = document.createElement("span");
|
|
21608
|
+
label.className = "tab-label";
|
|
21609
|
+
label.textContent = tab.title;
|
|
21610
|
+
tabButton.append(label, renderShareButton(tab), renderCloseButton(state, tab));
|
|
21611
|
+
return tabButton;
|
|
21243
21612
|
}
|
|
21244
|
-
function
|
|
21245
|
-
|
|
21613
|
+
function renderShareButton(tab) {
|
|
21614
|
+
const share = document.createElement("button");
|
|
21615
|
+
share.type = "button";
|
|
21616
|
+
share.className = "tab-share";
|
|
21617
|
+
share.setAttribute("aria-label", `Copy share link for ${tab.title}`);
|
|
21618
|
+
share.title = "Copy share link";
|
|
21619
|
+
share.textContent = "\u2197";
|
|
21620
|
+
share.addEventListener("click", (event) => {
|
|
21621
|
+
event.stopPropagation();
|
|
21622
|
+
void shareRuntimeTab(tab, share).catch((error) => console.error(error));
|
|
21623
|
+
});
|
|
21624
|
+
share.addEventListener("keydown", (event) => {
|
|
21625
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
21626
|
+
event.preventDefault();
|
|
21627
|
+
event.stopPropagation();
|
|
21628
|
+
void shareRuntimeTab(tab, share).catch((error) => console.error(error));
|
|
21629
|
+
});
|
|
21630
|
+
return share;
|
|
21246
21631
|
}
|
|
21247
|
-
function
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
|
|
21251
|
-
}
|
|
21252
|
-
|
|
21632
|
+
function renderCloseButton(state, tab) {
|
|
21633
|
+
const close = document.createElement("button");
|
|
21634
|
+
close.type = "button";
|
|
21635
|
+
close.className = "tab-close";
|
|
21636
|
+
close.setAttribute("aria-label", `Close ${tab.title}`);
|
|
21637
|
+
close.textContent = "x";
|
|
21638
|
+
close.addEventListener("click", (event) => {
|
|
21639
|
+
event.stopPropagation();
|
|
21640
|
+
state.closeTab(tab.id);
|
|
21641
|
+
});
|
|
21642
|
+
close.addEventListener("keydown", (event) => {
|
|
21643
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
21644
|
+
event.preventDefault();
|
|
21645
|
+
event.stopPropagation();
|
|
21646
|
+
state.closeTab(tab.id);
|
|
21647
|
+
});
|
|
21648
|
+
return close;
|
|
21253
21649
|
}
|
|
21254
|
-
function
|
|
21255
|
-
|
|
21256
|
-
|
|
21650
|
+
async function shareRuntimeTab(tab, button2) {
|
|
21651
|
+
const shareUrl = createPajaShareUrl(tab.pointerValue);
|
|
21652
|
+
const clipboard = navigator.clipboard;
|
|
21653
|
+
if (!clipboard) {
|
|
21654
|
+
window.prompt("Copy Paja share link", shareUrl);
|
|
21655
|
+
return;
|
|
21257
21656
|
}
|
|
21258
|
-
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
"
|
|
21262
|
-
|
|
21263
|
-
}
|
|
21657
|
+
try {
|
|
21658
|
+
await clipboard.writeText(shareUrl);
|
|
21659
|
+
} catch {
|
|
21660
|
+
window.prompt("Copy Paja share link", shareUrl);
|
|
21661
|
+
return;
|
|
21662
|
+
}
|
|
21663
|
+
button2.title = "Share link copied";
|
|
21664
|
+
window.setTimeout(() => {
|
|
21665
|
+
button2.title = "Copy share link";
|
|
21666
|
+
}, 1200);
|
|
21264
21667
|
}
|
|
21265
|
-
function
|
|
21266
|
-
return
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
...pointer.author ? { authors: [pointer.author] } : {},
|
|
21270
|
-
limit: 1
|
|
21271
|
-
};
|
|
21668
|
+
function getPointerParamName(pointer) {
|
|
21669
|
+
if (pointer.startsWith("naddr")) return "naddr";
|
|
21670
|
+
if (pointer.startsWith("nevent")) return "nevent";
|
|
21671
|
+
return "pointer";
|
|
21272
21672
|
}
|
|
21273
|
-
function
|
|
21274
|
-
|
|
21275
|
-
|
|
21276
|
-
|
|
21277
|
-
if (pointer.kind && event.kind !== pointer.kind) return false;
|
|
21278
|
-
return true;
|
|
21279
|
-
}
|
|
21280
|
-
return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
|
|
21673
|
+
function resolvedTargetTitle(target) {
|
|
21674
|
+
const titleTag = target.event.tags.find((tag) => tag[0] === "title")?.[1];
|
|
21675
|
+
const title = typeof target.manifest.title === "string" && target.manifest.title.trim().length > 0 ? target.manifest.title.trim() : titleTag?.trim();
|
|
21676
|
+
return title && title.length > 0 ? title : target.dTag || target.aggregateHash.slice(0, 12);
|
|
21281
21677
|
}
|
|
21282
|
-
function
|
|
21283
|
-
|
|
21284
|
-
|
|
21285
|
-
|
|
21286
|
-
|
|
21287
|
-
|
|
21288
|
-
|
|
21289
|
-
|
|
21678
|
+
function destroyRuntimeTab(tab, context) {
|
|
21679
|
+
context.onTabDestroyed?.(tab);
|
|
21680
|
+
if (tab.windowId) {
|
|
21681
|
+
context.bridge.runtime.destroyWindow(tab.windowId);
|
|
21682
|
+
context.bridge.runtime.sessionRegistry.unregister(tab.windowId);
|
|
21683
|
+
originRegistry.unregister(tab.windowId);
|
|
21684
|
+
context.runtime.readyWindowIds.delete(tab.windowId);
|
|
21685
|
+
if (context.runtime.currentWindowId === tab.windowId) context.runtime.currentWindowId = null;
|
|
21290
21686
|
}
|
|
21291
|
-
|
|
21687
|
+
tab.frame.remove();
|
|
21292
21688
|
}
|
|
21293
|
-
function
|
|
21294
|
-
const
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
|
|
21298
|
-
|
|
21299
|
-
|
|
21300
|
-
|
|
21301
|
-
|
|
21302
|
-
|
|
21303
|
-
|
|
21304
|
-
|
|
21689
|
+
function createRuntimeTabFrame(tab, state, context) {
|
|
21690
|
+
const frame = document.createElement("iframe");
|
|
21691
|
+
frame.id = `napplet-frame-${tab.id}`;
|
|
21692
|
+
frame.className = "tab-panel";
|
|
21693
|
+
frame.title = `Napplet runtime target: ${tab.title}`;
|
|
21694
|
+
frame.sandbox.add("allow-scripts");
|
|
21695
|
+
frame.sandbox.remove("allow-same-origin");
|
|
21696
|
+
frame.dataset.tabId = tab.id;
|
|
21697
|
+
frame.dataset.targetUrl = tab.pointerValue;
|
|
21698
|
+
frame.hidden = true;
|
|
21699
|
+
frame.addEventListener("error", () => {
|
|
21700
|
+
tab.status = "error";
|
|
21701
|
+
if (state.activeTabId === tab.id) context.setStatus(state, "error");
|
|
21702
|
+
renderRuntimeTabs(state);
|
|
21703
|
+
});
|
|
21704
|
+
return frame;
|
|
21305
21705
|
}
|
|
21306
|
-
function
|
|
21307
|
-
|
|
21706
|
+
function startRuntimeTabNavigation(tab, state, context) {
|
|
21707
|
+
const generation = tab.generation;
|
|
21708
|
+
tab.status = "booting";
|
|
21709
|
+
context.setStatus(state, "booting");
|
|
21710
|
+
renderRuntimeTabs(state);
|
|
21711
|
+
void context.navigateFrame(
|
|
21712
|
+
tab.frame,
|
|
21713
|
+
context.config,
|
|
21714
|
+
generation,
|
|
21715
|
+
context.adapter,
|
|
21716
|
+
tab.resolvedTarget,
|
|
21717
|
+
runtimeTabWindowId(context.config, tab),
|
|
21718
|
+
() => tab.generation === generation,
|
|
21719
|
+
(windowId) => {
|
|
21720
|
+
if (tab.generation !== generation) return;
|
|
21721
|
+
tab.windowId = windowId;
|
|
21722
|
+
if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
|
|
21723
|
+
}
|
|
21724
|
+
).then((windowId) => {
|
|
21725
|
+
if (tab.generation !== generation) return;
|
|
21726
|
+
tab.windowId = windowId;
|
|
21727
|
+
if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
|
|
21728
|
+
}).catch((error) => {
|
|
21729
|
+
if (tab.generation !== generation) return;
|
|
21730
|
+
tab.frame.removeAttribute("src");
|
|
21731
|
+
tab.frame.srcdoc = context.renderTargetErrorHtml(error);
|
|
21732
|
+
tab.status = "error";
|
|
21733
|
+
if (state.activeTabId === tab.id) context.setStatus(state, "error");
|
|
21734
|
+
appendPajaMessageLog(state, "paja", {
|
|
21735
|
+
type: "paja.target.error",
|
|
21736
|
+
error: error instanceof Error ? error.message : String(error)
|
|
21737
|
+
}, tab.windowId ?? void 0);
|
|
21738
|
+
renderRuntimeTabs(state);
|
|
21739
|
+
console.error(error);
|
|
21740
|
+
});
|
|
21308
21741
|
}
|
|
21309
|
-
function
|
|
21310
|
-
return
|
|
21742
|
+
function runtimeTabWindowId(config, tab) {
|
|
21743
|
+
return `${config.window.id}:${tab.id}:${tab.generation}`;
|
|
21311
21744
|
}
|
|
21312
21745
|
|
|
21313
21746
|
// src/browser-intent-host.ts
|
|
@@ -21489,113 +21922,6 @@ function createPajaThemeBroadcastLink() {
|
|
|
21489
21922
|
};
|
|
21490
21923
|
}
|
|
21491
21924
|
|
|
21492
|
-
// src/browser-target-frame.ts
|
|
21493
|
-
function resolvePajaFrameEnvironment(hooks, identity) {
|
|
21494
|
-
if (!identity.dTag || !identity.aggregateHash) {
|
|
21495
|
-
throw new Error("Paja frame identity requires a dTag and aggregate hash");
|
|
21496
|
-
}
|
|
21497
|
-
return resolveShellEnvironment(hooks, identity);
|
|
21498
|
-
}
|
|
21499
|
-
function getTargetIdentity2(config, resolvedTarget) {
|
|
21500
|
-
return {
|
|
21501
|
-
pubkey: "",
|
|
21502
|
-
dTag: resolvedTarget?.dTag ?? config.window.dTag,
|
|
21503
|
-
aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
|
|
21504
|
-
};
|
|
21505
|
-
}
|
|
21506
|
-
function getTargetOriginIdentity(config, resolvedTarget) {
|
|
21507
|
-
const target = getTargetIdentity2(config, resolvedTarget);
|
|
21508
|
-
return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
|
|
21509
|
-
}
|
|
21510
|
-
function registerFrameForGeneration(frame, config, generation, identity, environment, windowId = `${config.window.id}:${generation}`) {
|
|
21511
|
-
const win = frame.contentWindow;
|
|
21512
|
-
if (!win) return null;
|
|
21513
|
-
originRegistry.register(win, windowId, identity);
|
|
21514
|
-
originRegistry.setEnvironment(win, environment);
|
|
21515
|
-
return windowId;
|
|
21516
|
-
}
|
|
21517
|
-
async function navigateFrame(frame, config, generation, adapter, resolvedTarget, windowId, isCurrent, onRegistered) {
|
|
21518
|
-
const identity = getTargetOriginIdentity(config, resolvedTarget);
|
|
21519
|
-
const environment = resolvePajaFrameEnvironment(adapter, identity);
|
|
21520
|
-
const domains = environment.capabilities.domains;
|
|
21521
|
-
if (config.target.mode === "runtime-pointer") {
|
|
21522
|
-
if (!resolvedTarget) {
|
|
21523
|
-
frame.removeAttribute("src");
|
|
21524
|
-
frame.srcdoc = "<!doctype html><html><body></body></html>";
|
|
21525
|
-
return null;
|
|
21526
|
-
}
|
|
21527
|
-
if (isCurrent && !isCurrent()) return null;
|
|
21528
|
-
const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
|
|
21529
|
-
onRegistered?.(registeredWindowId2);
|
|
21530
|
-
frame.removeAttribute("src");
|
|
21531
|
-
frame.srcdoc = injectNappletNamespacePrelude(
|
|
21532
|
-
injectPajaRuntimeCsp(
|
|
21533
|
-
resolvedTarget.indexHtml,
|
|
21534
|
-
connectOrigins([...resolvedTarget.relays, ...resolvedTarget.blossomServers])
|
|
21535
|
-
),
|
|
21536
|
-
{ domains }
|
|
21537
|
-
);
|
|
21538
|
-
return registeredWindowId2;
|
|
21539
|
-
}
|
|
21540
|
-
const html = await fetchTargetHtml();
|
|
21541
|
-
if (isCurrent && !isCurrent()) return null;
|
|
21542
|
-
const registeredWindowId = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
|
|
21543
|
-
onRegistered?.(registeredWindowId);
|
|
21544
|
-
frame.removeAttribute("src");
|
|
21545
|
-
frame.srcdoc = injectNappletNamespacePrelude(
|
|
21546
|
-
injectBaseHref(html, config.target.url),
|
|
21547
|
-
{ domains }
|
|
21548
|
-
);
|
|
21549
|
-
return registeredWindowId;
|
|
21550
|
-
}
|
|
21551
|
-
function renderTargetErrorHtml(error) {
|
|
21552
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
21553
|
-
return `<!doctype html><html><body><pre>${escapeHtml(message)}</pre></body></html>`;
|
|
21554
|
-
}
|
|
21555
|
-
function connectOrigins(urls) {
|
|
21556
|
-
const out = /* @__PURE__ */ new Set();
|
|
21557
|
-
for (const value of urls) {
|
|
21558
|
-
try {
|
|
21559
|
-
const url = new URL(value);
|
|
21560
|
-
if (url.protocol === "wss:" || url.protocol === "ws:") {
|
|
21561
|
-
out.add(value.replace(/\/$/, ""));
|
|
21562
|
-
} else {
|
|
21563
|
-
out.add(url.origin);
|
|
21564
|
-
}
|
|
21565
|
-
} catch {
|
|
21566
|
-
}
|
|
21567
|
-
}
|
|
21568
|
-
return [...out];
|
|
21569
|
-
}
|
|
21570
|
-
async function fetchTargetHtml() {
|
|
21571
|
-
const response = await fetch(new URL("./__kehto/target.html", window.location.href), {
|
|
21572
|
-
cache: "no-store",
|
|
21573
|
-
headers: {
|
|
21574
|
-
accept: "text/html, application/xhtml+xml;q=0.9, */*;q=0.8"
|
|
21575
|
-
}
|
|
21576
|
-
});
|
|
21577
|
-
if (!response.ok) {
|
|
21578
|
-
throw new Error(`Paja target fetch failed: ${response.status} ${response.statusText}`);
|
|
21579
|
-
}
|
|
21580
|
-
return response.text();
|
|
21581
|
-
}
|
|
21582
|
-
function injectBaseHref(html, targetUrl) {
|
|
21583
|
-
const base = `<base href="${escapeAttribute2(targetUrl)}">`;
|
|
21584
|
-
if (/<head[^>]*>/i.test(html)) {
|
|
21585
|
-
return html.replace(/<head[^>]*>/i, (open) => `${open}${base}`);
|
|
21586
|
-
}
|
|
21587
|
-
if (/<html[^>]*>/i.test(html)) {
|
|
21588
|
-
return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${base}</head>`);
|
|
21589
|
-
}
|
|
21590
|
-
return `${base}${html}`;
|
|
21591
|
-
}
|
|
21592
|
-
function escapeAttribute2(value) {
|
|
21593
|
-
return escapeHtml(value).replaceAll('"', """);
|
|
21594
|
-
}
|
|
21595
|
-
function escapeHtml(value) {
|
|
21596
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
21597
|
-
}
|
|
21598
|
-
|
|
21599
21925
|
// src/browser-target-diagnostics.ts
|
|
21600
21926
|
async function reportTargetCorsDiagnostic(state) {
|
|
21601
21927
|
let diagnostic;
|
|
@@ -22316,6 +22642,7 @@ function snapshotPajaBrowserState(state, runtime) {
|
|
|
22316
22642
|
services: state.services,
|
|
22317
22643
|
simulation: runtime.currentSimulation,
|
|
22318
22644
|
signer: state.signer,
|
|
22645
|
+
signerConsentCount: state.signerConsentCount,
|
|
22319
22646
|
resolvedTarget: state.resolvedTarget,
|
|
22320
22647
|
pointerStatus: state.pointerStatus,
|
|
22321
22648
|
activeTabId: state.activeTabId,
|
|
@@ -22331,13 +22658,22 @@ function snapshotPajaBrowserState(state, runtime) {
|
|
|
22331
22658
|
};
|
|
22332
22659
|
}
|
|
22333
22660
|
function createPajaBrowserState(context) {
|
|
22334
|
-
const {
|
|
22661
|
+
const {
|
|
22662
|
+
config,
|
|
22663
|
+
bridge,
|
|
22664
|
+
adapter,
|
|
22665
|
+
signerController,
|
|
22666
|
+
confirmationController,
|
|
22667
|
+
capabilities,
|
|
22668
|
+
runtime
|
|
22669
|
+
} = context;
|
|
22335
22670
|
return {
|
|
22336
22671
|
config,
|
|
22337
22672
|
capabilities,
|
|
22338
22673
|
services: Object.keys(adapter.services ?? {}).sort(),
|
|
22339
22674
|
simulation: runtime.currentSimulation,
|
|
22340
22675
|
signer: signerController.getState(),
|
|
22676
|
+
signerConsentCount: confirmationController.getSignerConsentCount(),
|
|
22341
22677
|
resolvedTarget: null,
|
|
22342
22678
|
pointerValue: readInitialPointerValue(config),
|
|
22343
22679
|
pointerStatus: config.target.mode === "runtime-pointer" ? "idle" : "",
|
|
@@ -22374,7 +22710,7 @@ function createPajaBrowserState(context) {
|
|
|
22374
22710
|
setRuntimeDomainEnabled(this, context, domain, enabled);
|
|
22375
22711
|
},
|
|
22376
22712
|
setAclCapability(capability, enabled) {
|
|
22377
|
-
const identity =
|
|
22713
|
+
const identity = getTargetIdentity(config, this.resolvedTarget);
|
|
22378
22714
|
if (enabled) bridge.runtime.aclState.grant(identity.pubkey, identity.dTag, identity.aggregateHash, capability);
|
|
22379
22715
|
else bridge.runtime.aclState.revoke(identity.pubkey, identity.dTag, identity.aggregateHash, capability);
|
|
22380
22716
|
bridge.runtime.aclState.persist();
|
|
@@ -22390,6 +22726,12 @@ function createPajaBrowserState(context) {
|
|
|
22390
22726
|
connectBunker(uri) {
|
|
22391
22727
|
return signerController.connectBunker(uri);
|
|
22392
22728
|
},
|
|
22729
|
+
clearSignerConsent() {
|
|
22730
|
+
const cleared = confirmationController.clearSignerConsent();
|
|
22731
|
+
appendPajaMessageLog(this, "paja", {
|
|
22732
|
+
type: cleared ? "paja.signer-consent.cleared" : "paja.signer-consent.clear-failed"
|
|
22733
|
+
});
|
|
22734
|
+
},
|
|
22393
22735
|
async loadPointer(value) {
|
|
22394
22736
|
await loadRuntimePointer(this, context, value);
|
|
22395
22737
|
},
|
|
@@ -22425,7 +22767,16 @@ async function installPajaHost() {
|
|
|
22425
22767
|
() => contextRef,
|
|
22426
22768
|
{ persistTabs: persistRuntimeTabs }
|
|
22427
22769
|
));
|
|
22428
|
-
const confirmationController = createPajaConfirmationController(
|
|
22770
|
+
const confirmationController = createPajaConfirmationController(
|
|
22771
|
+
() => stateRef,
|
|
22772
|
+
{
|
|
22773
|
+
onSignerConsentChange(count) {
|
|
22774
|
+
if (!stateRef) return;
|
|
22775
|
+
stateRef.signerConsentCount = count;
|
|
22776
|
+
setSimulationStatus(stateRef);
|
|
22777
|
+
}
|
|
22778
|
+
}
|
|
22779
|
+
);
|
|
22429
22780
|
const signerController = createHostSignerController(
|
|
22430
22781
|
() => stateRef,
|
|
22431
22782
|
setSimulationStatus,
|
|
@@ -22433,7 +22784,7 @@ async function installPajaHost() {
|
|
|
22433
22784
|
);
|
|
22434
22785
|
const getWindowIdentity = (windowId) => {
|
|
22435
22786
|
const tabTarget = stateRef?.tabs.find((tab) => tab.windowId === windowId)?.resolvedTarget;
|
|
22436
|
-
return
|
|
22787
|
+
return getTargetIdentity(config, tabTarget ?? stateRef?.resolvedTarget);
|
|
22437
22788
|
};
|
|
22438
22789
|
const notifyController = createPajaNotifyController({
|
|
22439
22790
|
confirm: confirmationController.confirm,
|
|
@@ -22466,6 +22817,7 @@ async function installPajaHost() {
|
|
|
22466
22817
|
bridge,
|
|
22467
22818
|
adapter,
|
|
22468
22819
|
signerController,
|
|
22820
|
+
confirmationController,
|
|
22469
22821
|
capabilities,
|
|
22470
22822
|
runtime,
|
|
22471
22823
|
navigateFrame,
|