@kehto/paja 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -11
- package/dist/browser-host.js +101 -33
- package/dist/browser-host.js.map +1 -1
- package/dist/{chunk-DTRRHXYY.js → chunk-32QBJDAR.js} +2 -2
- package/dist/chunk-32QBJDAR.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +10 -10
- package/dist/chunk-DTRRHXYY.js.map +0 -1
package/README.md
CHANGED
|
@@ -156,7 +156,7 @@ outside this behavior.
|
|
|
156
156
|
[NAP-IDENTITY at `6461e4b37c29dc09a20dff35d9515889c4433874`](https://github.com/napplet/naps/blob/6461e4b37c29dc09a20dff35d9515889c4433874/naps/NAP-IDENTITY.md)
|
|
157
157
|
is byte-identical to the recorded `napplet/naps` master document for this phase.
|
|
158
158
|
Pinned [NAP-OUTBOX at `4589a8f9a16d8aa29b3740e2b3b0cdca11e0976e`](https://github.com/napplet/naps/blob/4589a8f9a16d8aa29b3740e2b3b0cdca11e0976e/naps/NAP-OUTBOX.md)
|
|
159
|
-
together with installed `@napplet/nap@0.
|
|
159
|
+
together with installed `@napplet/nap@0.32.0` types is the PoC contract because
|
|
160
160
|
current master has no NAP-OUTBOX path. Paja therefore makes no current-master
|
|
161
161
|
OUTBOX conformance claim. Blossom upload behavior targets pinned
|
|
162
162
|
[NAP-UPLOAD at `a7cc17463cbf5d9cb87884b31071bc4fc826034c`](https://github.com/napplet/naps/blob/a7cc17463cbf5d9cb87884b31071bc4fc826034c/naps/NAP-UPLOAD.md).
|
|
@@ -210,7 +210,7 @@ identity. A superseded target/source, failed open/readiness, or terminal send is
|
|
|
210
210
|
handled by the controller's replacement/retry/terminal policy and produces a
|
|
211
211
|
canonical failed `IntentResult`.
|
|
212
212
|
|
|
213
|
-
`@napplet/shim@0.
|
|
213
|
+
`@napplet/shim@0.30.0` supplies no generic shell API. Kehto deliberately keeps
|
|
214
214
|
its host-owned mandatory `window.napplet.shell` prelude: it installs the live
|
|
215
215
|
receiver before the one bare `shell.ready`, caches the first `shell.init`, and
|
|
216
216
|
provides local `ready()`, `supports()`, read-only `services`, and one-shot
|
|
@@ -219,8 +219,9 @@ shim capability.
|
|
|
219
219
|
|
|
220
220
|
Before Paja assigns a verified runtime-pointer document to `srcdoc`, it inserts
|
|
221
221
|
Kehto's local Class-1 CSP before the host-owned namespace prelude. The policy
|
|
222
|
-
denies all defaults; permits inline script/style,
|
|
223
|
-
|
|
222
|
+
denies all defaults; permits inline script/style, WebAssembly compilation through
|
|
223
|
+
the narrow `'wasm-unsafe-eval'` source, `data:`/`blob:` images, and `data:` fonts;
|
|
224
|
+
keeps JavaScript string evaluation blocked; grants `connect-src` only to the resolved relay and Blossom
|
|
224
225
|
origins; explicitly denies worker, child, frame, media, object, manifest,
|
|
225
226
|
prefetch, base, and form capabilities; and ends with `frame-ancestors 'self'`.
|
|
226
227
|
The NIP-5D verified-srcdoc and opaque-sandbox rules do not mandate this CSP;
|
|
@@ -310,13 +311,19 @@ canonical `network-error`, while any CORS-readable response is returned as NAP
|
|
|
310
311
|
bytes.
|
|
311
312
|
|
|
312
313
|
`data:` remains locally decoded. `blossom:` is a separate, content-addressed
|
|
313
|
-
boundary and is advertised
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
314
|
+
boundary and is advertised because each request may provide server locations
|
|
315
|
+
without a host default. Paja accepts only public-looking HTTPS origin hints,
|
|
316
|
+
discards invalid/private literals, deduplicates them, and tries them before
|
|
317
|
+
host-configured runtime-pointer or upload defaults. The combined list is capped
|
|
318
|
+
at eight candidates. The only accepted identifier is `blossom:sha256:<hex>`;
|
|
319
|
+
Paja refuses redirects, verifies the requested SHA-256, and permits plain-HTTP
|
|
320
|
+
transport only for configured loopback development defaults. Browser-only Paja
|
|
321
|
+
cannot pin DNS results, so production runtimes must add the draft's DNS-time
|
|
322
|
+
private-address checks. This behavior targets draft
|
|
323
|
+
[NAP-RESOURCE at `9511232`](https://github.com/napplet/naps/blob/9511232f69313aa7953d110e35d32cc28d506f66/naps/NAP-RESOURCE.md), with the merged package implementation
|
|
324
|
+
[`napplet/web#206@19e0029b`](https://github.com/napplet/web/pull/206) released as
|
|
325
|
+
`@napplet/core`/`@napplet/nap` 0.32.0, `@napplet/shim` 0.30.0, and
|
|
326
|
+
`@napplet/sdk` 0.28.0.
|
|
320
327
|
|
|
321
328
|
Full package docs: [`docs/packages/paja.md`](../../docs/packages/paja.md).
|
|
322
329
|
Getting started: [`docs/how-tos/paja-getting-started.md`](../../docs/how-tos/paja-getting-started.md).
|
package/dist/browser-host.js
CHANGED
|
@@ -638,7 +638,7 @@ function deserialize2(json) {
|
|
|
638
638
|
return defaultConfig();
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
-
// ../../node_modules/.pnpm/@napplet+core@0.
|
|
641
|
+
// ../../node_modules/.pnpm/@napplet+core@0.32.0/node_modules/@napplet/core/dist/index.js
|
|
642
642
|
function createDispatch() {
|
|
643
643
|
const handlers = /* @__PURE__ */ new Map();
|
|
644
644
|
function registerNap2(domain, handler) {
|
|
@@ -4802,11 +4802,21 @@ function nappletNamespacePrelude(domains) {
|
|
|
4802
4802
|
}
|
|
4803
4803
|
function makeResource() {
|
|
4804
4804
|
const objectUrls = /* @__PURE__ */ new Set();
|
|
4805
|
-
const bytes = (url) => request({
|
|
4805
|
+
const bytes = (url, options) => request({
|
|
4806
|
+
type: "resource.bytes",
|
|
4807
|
+
url,
|
|
4808
|
+
...Array.isArray(options?.servers) ? { servers: Array.from(options.servers) } : {}
|
|
4809
|
+
}, "resource.bytes.result", (msg) => msg.blob);
|
|
4806
4810
|
return {
|
|
4807
4811
|
info: () => request({ type: "resource.info" }, "resource.info.result", (msg) => msg.info),
|
|
4808
4812
|
bytes,
|
|
4809
|
-
bytesMany: (
|
|
4813
|
+
bytesMany: (requests) => request({
|
|
4814
|
+
type: "resource.bytesMany",
|
|
4815
|
+
requests: Array.from(requests || [], (entry) => ({
|
|
4816
|
+
url: entry.url,
|
|
4817
|
+
...Array.isArray(entry.servers) ? { servers: Array.from(entry.servers) } : {}
|
|
4818
|
+
}))
|
|
4819
|
+
}, "resource.bytesMany.result", (msg) => Array.isArray(msg.items) ? msg.items : []),
|
|
4810
4820
|
bytesAsObjectURL(url) {
|
|
4811
4821
|
let objectUrl = "";
|
|
4812
4822
|
const handle = {
|
|
@@ -11155,7 +11165,8 @@ function normalizeResourceInfo(info) {
|
|
|
11155
11165
|
return {
|
|
11156
11166
|
schemes: Array.isArray(info.schemes) ? info.schemes.filter((scheme) => typeof scheme?.scheme === "string").map((scheme) => ({ scheme: scheme.scheme, enabled: scheme.enabled !== false })) : [],
|
|
11157
11167
|
...typeof info.maxBytes === "number" ? { maxBytes: info.maxBytes } : {},
|
|
11158
|
-
...typeof info.maxUrls === "number" ? { maxUrls: info.maxUrls } : {}
|
|
11168
|
+
...typeof info.maxUrls === "number" ? { maxUrls: info.maxUrls } : {},
|
|
11169
|
+
...typeof info.maxServers === "number" ? { maxServers: info.maxServers } : {}
|
|
11159
11170
|
};
|
|
11160
11171
|
}
|
|
11161
11172
|
async function resolveResourceInfo(options, windowId) {
|
|
@@ -11194,10 +11205,11 @@ function classifyResourceFailure(error) {
|
|
|
11194
11205
|
message: error instanceof Error ? error.message : String(error)
|
|
11195
11206
|
};
|
|
11196
11207
|
}
|
|
11197
|
-
async function fetchResourceItem(options, windowId, info,
|
|
11208
|
+
async function fetchResourceItem(options, windowId, info, request, signal) {
|
|
11209
|
+
const { url } = request;
|
|
11198
11210
|
const parsedUrl = parseResourceUrl(url);
|
|
11199
11211
|
if (!parsedUrl) return resourceInvalidRequest(url, `invalid URL: ${url}`);
|
|
11200
|
-
if (options.isOriginGranted && options.getConnectGrants) {
|
|
11212
|
+
if (parsedUrl.origin !== "null" && options.isOriginGranted && options.getConnectGrants) {
|
|
11201
11213
|
const identity = options.resolveIdentity?.(windowId) ?? null;
|
|
11202
11214
|
if (!identity) {
|
|
11203
11215
|
return {
|
|
@@ -11221,7 +11233,8 @@ async function fetchResourceItem(options, windowId, info, url, signal) {
|
|
|
11221
11233
|
try {
|
|
11222
11234
|
const response = await options.fetch(url, {
|
|
11223
11235
|
method: "GET",
|
|
11224
|
-
signal
|
|
11236
|
+
signal,
|
|
11237
|
+
...parsedUrl.protocol === "blossom:" && request.servers !== void 0 ? { servers: [...request.servers] } : {}
|
|
11225
11238
|
});
|
|
11226
11239
|
const buffer = await response.arrayBuffer();
|
|
11227
11240
|
if (info.maxBytes !== void 0 && buffer.byteLength > info.maxBytes) {
|
|
@@ -11245,12 +11258,12 @@ async function fetchResourceItem(options, windowId, info, url, signal) {
|
|
|
11245
11258
|
}
|
|
11246
11259
|
}
|
|
11247
11260
|
async function handleBytes(options, state, windowId, msg, send) {
|
|
11248
|
-
const { requestId,
|
|
11261
|
+
const { requestId, ...request } = msg;
|
|
11249
11262
|
const controller = new AbortController();
|
|
11250
11263
|
trackRequest(state, requestId, windowId, controller);
|
|
11251
11264
|
try {
|
|
11252
11265
|
const info = await resolveResourceInfo(options, windowId);
|
|
11253
|
-
const item = await fetchResourceItem(options, windowId, info,
|
|
11266
|
+
const item = await fetchResourceItem(options, windowId, info, request, controller.signal);
|
|
11254
11267
|
if (controller.signal.aborted) return;
|
|
11255
11268
|
if (!item.ok) {
|
|
11256
11269
|
sendResourceError(send, requestId, item.message, item.error);
|
|
@@ -11272,9 +11285,9 @@ async function handleBytes(options, state, windowId, msg, send) {
|
|
|
11272
11285
|
}
|
|
11273
11286
|
}
|
|
11274
11287
|
async function handleBytesMany(options, state, windowId, msg, send) {
|
|
11275
|
-
const { requestId,
|
|
11276
|
-
if (!
|
|
11277
|
-
sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty
|
|
11288
|
+
const { requestId, requests } = msg;
|
|
11289
|
+
if (!isResourceRequestList(requests)) {
|
|
11290
|
+
sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty requests array", "invalid-request");
|
|
11278
11291
|
return;
|
|
11279
11292
|
}
|
|
11280
11293
|
let info;
|
|
@@ -11285,7 +11298,7 @@ async function handleBytesMany(options, state, windowId, msg, send) {
|
|
|
11285
11298
|
sendBytesManyError(send, requestId, failure3.message, failure3.error);
|
|
11286
11299
|
return;
|
|
11287
11300
|
}
|
|
11288
|
-
if (info.maxUrls !== void 0 &&
|
|
11301
|
+
if (info.maxUrls !== void 0 && requests.length > info.maxUrls) {
|
|
11289
11302
|
sendBytesManyError(send, requestId, `resource.bytesMany exceeds ${info.maxUrls} URLs`, "too-large");
|
|
11290
11303
|
return;
|
|
11291
11304
|
}
|
|
@@ -11293,8 +11306,8 @@ async function handleBytesMany(options, state, windowId, msg, send) {
|
|
|
11293
11306
|
trackRequest(state, requestId, windowId, controller);
|
|
11294
11307
|
try {
|
|
11295
11308
|
const items = [];
|
|
11296
|
-
for (const
|
|
11297
|
-
const item = await fetchResourceItem(options, windowId, info,
|
|
11309
|
+
for (const request of requests) {
|
|
11310
|
+
const item = await fetchResourceItem(options, windowId, info, request, controller.signal);
|
|
11298
11311
|
if (controller.signal.aborted) return;
|
|
11299
11312
|
if (item.ok) {
|
|
11300
11313
|
items.push({
|
|
@@ -11326,6 +11339,12 @@ async function handleBytesMany(options, state, windowId, msg, send) {
|
|
|
11326
11339
|
untrackRequest(state, windowId, requestId);
|
|
11327
11340
|
}
|
|
11328
11341
|
}
|
|
11342
|
+
function hasValidServerHints(request) {
|
|
11343
|
+
return request.servers === void 0 || Array.isArray(request.servers) && request.servers.every((server) => typeof server === "string");
|
|
11344
|
+
}
|
|
11345
|
+
function isResourceRequestList(value) {
|
|
11346
|
+
return Array.isArray(value) && value.length > 0 && value.every((request) => typeof request === "object" && request !== null && typeof request.url === "string" && hasValidServerHints(request));
|
|
11347
|
+
}
|
|
11329
11348
|
function handleCancel(state, windowId, requestId) {
|
|
11330
11349
|
const entry = state.inFlight.get(requestKey(windowId, requestId));
|
|
11331
11350
|
if (entry) {
|
|
@@ -11363,11 +11382,15 @@ function createResourceService(options) {
|
|
|
11363
11382
|
const m = message;
|
|
11364
11383
|
const requestId = requestIdFromMessage(m);
|
|
11365
11384
|
if (!requestId) return;
|
|
11366
|
-
if (typeof m.url !== "string") {
|
|
11367
|
-
sendResourceError(send, requestId, "resource.bytes requires a URL", "invalid-request");
|
|
11385
|
+
if (typeof m.url !== "string" || !hasValidServerHints(m)) {
|
|
11386
|
+
sendResourceError(send, requestId, "resource.bytes requires a URL and optional string servers", "invalid-request");
|
|
11368
11387
|
return;
|
|
11369
11388
|
}
|
|
11370
|
-
handleBytes(options, state, windowId, {
|
|
11389
|
+
handleBytes(options, state, windowId, {
|
|
11390
|
+
requestId,
|
|
11391
|
+
url: m.url,
|
|
11392
|
+
...m.servers !== void 0 ? { servers: [...m.servers] } : {}
|
|
11393
|
+
}, send).catch(() => {
|
|
11371
11394
|
});
|
|
11372
11395
|
return;
|
|
11373
11396
|
}
|
|
@@ -11383,7 +11406,7 @@ function createResourceService(options) {
|
|
|
11383
11406
|
const m = message;
|
|
11384
11407
|
const requestId = requestIdFromMessage(m);
|
|
11385
11408
|
if (!requestId) return;
|
|
11386
|
-
handleBytesMany(options, state, windowId, { requestId,
|
|
11409
|
+
handleBytesMany(options, state, windowId, { requestId, requests: m.requests ?? [] }, send).catch(() => {
|
|
11387
11410
|
});
|
|
11388
11411
|
return;
|
|
11389
11412
|
}
|
|
@@ -18184,6 +18207,7 @@ async function resolveNapplet(options) {
|
|
|
18184
18207
|
// src/browser-resource.ts
|
|
18185
18208
|
var PAJA_RESOURCE_MAX_BYTES = 10 * 1024 * 1024;
|
|
18186
18209
|
var PAJA_RESOURCE_MAX_URLS = 100;
|
|
18210
|
+
var PAJA_RESOURCE_MAX_SERVERS = 8;
|
|
18187
18211
|
var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
18188
18212
|
var BLOSSOM_RESOURCE_PATTERN = /^blossom:sha256:([0-9a-f]{64})$/i;
|
|
18189
18213
|
function createPajaResourceFetch(options = {}) {
|
|
@@ -18194,7 +18218,7 @@ function createPajaResourceFetch(options = {}) {
|
|
|
18194
18218
|
throw new ResourceServiceError("invalid-request", "NAP-RESOURCE is read-only");
|
|
18195
18219
|
}
|
|
18196
18220
|
if (url.protocol === "blossom:") {
|
|
18197
|
-
return fetchBlossomResource(value, init.signal, options);
|
|
18221
|
+
return fetchBlossomResource(value, init.signal, init.servers ?? [], options);
|
|
18198
18222
|
}
|
|
18199
18223
|
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
18200
18224
|
return fetchNetworkResource(value, init.signal, options.fetch ?? globalThis.fetch);
|
|
@@ -18217,19 +18241,18 @@ function createPajaResourceFetch(options = {}) {
|
|
|
18217
18241
|
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
18218
18242
|
};
|
|
18219
18243
|
}
|
|
18220
|
-
function pajaResourceInfo(
|
|
18244
|
+
function pajaResourceInfo() {
|
|
18221
18245
|
const schemes = [
|
|
18222
18246
|
{ scheme: "data", enabled: true },
|
|
18223
18247
|
{ scheme: "https", enabled: true },
|
|
18224
|
-
{ scheme: "http", enabled: true }
|
|
18248
|
+
{ scheme: "http", enabled: true },
|
|
18249
|
+
{ scheme: "blossom", enabled: true }
|
|
18225
18250
|
];
|
|
18226
|
-
if (usableBlossomServers(blossomServers).length > 0) {
|
|
18227
|
-
schemes.push({ scheme: "blossom", enabled: true });
|
|
18228
|
-
}
|
|
18229
18251
|
return {
|
|
18230
18252
|
schemes,
|
|
18231
18253
|
maxBytes: PAJA_RESOURCE_MAX_BYTES,
|
|
18232
|
-
maxUrls: PAJA_RESOURCE_MAX_URLS
|
|
18254
|
+
maxUrls: PAJA_RESOURCE_MAX_URLS,
|
|
18255
|
+
maxServers: PAJA_RESOURCE_MAX_SERVERS
|
|
18233
18256
|
};
|
|
18234
18257
|
}
|
|
18235
18258
|
async function fetchNetworkResource(value, signal, fetcher) {
|
|
@@ -18261,20 +18284,24 @@ async function fetchNetworkResource(value, signal, fetcher) {
|
|
|
18261
18284
|
}
|
|
18262
18285
|
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
18263
18286
|
}
|
|
18264
|
-
async function fetchBlossomResource(value, signal, options) {
|
|
18287
|
+
async function fetchBlossomResource(value, signal, requestServers, options) {
|
|
18265
18288
|
const match = BLOSSOM_RESOURCE_PATTERN.exec(value);
|
|
18266
18289
|
if (!match?.[1]) {
|
|
18267
18290
|
throw new ResourceServiceError("invalid-request", "expected blossom:sha256:<64 hex characters>");
|
|
18268
18291
|
}
|
|
18269
18292
|
const expectedHash = match[1].toLowerCase();
|
|
18270
|
-
const servers =
|
|
18293
|
+
const servers = resolveBlossomServers(
|
|
18294
|
+
requestServers,
|
|
18295
|
+
options.getBlossomServers?.() ?? []
|
|
18296
|
+
);
|
|
18271
18297
|
if (servers.length === 0) {
|
|
18272
|
-
throw new ResourceServiceError("blocked-by-policy", "Paja has no
|
|
18298
|
+
throw new ResourceServiceError("blocked-by-policy", "Paja has no accepted Blossom server");
|
|
18273
18299
|
}
|
|
18274
18300
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
18275
18301
|
let foundHashMismatch = false;
|
|
18276
18302
|
let foundNotFound = false;
|
|
18277
|
-
let
|
|
18303
|
+
let foundInconclusive = false;
|
|
18304
|
+
let lastNetworkMessage = "all accepted Blossom servers failed";
|
|
18278
18305
|
for (const server of servers) {
|
|
18279
18306
|
if (signal.aborted) throw new DOMException("Resource request cancelled", "AbortError");
|
|
18280
18307
|
const resourceUrl = `${server}/${expectedHash}`;
|
|
@@ -18284,10 +18311,13 @@ async function fetchBlossomResource(value, signal, options) {
|
|
|
18284
18311
|
method: "GET",
|
|
18285
18312
|
signal,
|
|
18286
18313
|
redirect: "error",
|
|
18287
|
-
cache: "no-store"
|
|
18314
|
+
cache: "no-store",
|
|
18315
|
+
credentials: "omit",
|
|
18316
|
+
referrerPolicy: "no-referrer"
|
|
18288
18317
|
});
|
|
18289
18318
|
} catch (error) {
|
|
18290
18319
|
if (signal.aborted || isAbortError(error)) throw error;
|
|
18320
|
+
foundInconclusive = true;
|
|
18291
18321
|
lastNetworkMessage = error instanceof Error ? error.message : String(error);
|
|
18292
18322
|
continue;
|
|
18293
18323
|
}
|
|
@@ -18296,6 +18326,7 @@ async function fetchBlossomResource(value, signal, options) {
|
|
|
18296
18326
|
continue;
|
|
18297
18327
|
}
|
|
18298
18328
|
if (!response.ok) {
|
|
18329
|
+
foundInconclusive = true;
|
|
18299
18330
|
lastNetworkMessage = `Blossom server returned HTTP ${response.status}`;
|
|
18300
18331
|
continue;
|
|
18301
18332
|
}
|
|
@@ -18313,11 +18344,48 @@ async function fetchBlossomResource(value, signal, options) {
|
|
|
18313
18344
|
if (foundHashMismatch) {
|
|
18314
18345
|
throw new ResourceServiceError("decode-failed", "Blossom response did not match the requested SHA-256");
|
|
18315
18346
|
}
|
|
18347
|
+
if (foundInconclusive) {
|
|
18348
|
+
throw new ResourceServiceError("network-error", lastNetworkMessage);
|
|
18349
|
+
}
|
|
18316
18350
|
if (foundNotFound) {
|
|
18317
18351
|
throw new ResourceServiceError("not-found", "Blossom resource was not found");
|
|
18318
18352
|
}
|
|
18319
18353
|
throw new ResourceServiceError("network-error", lastNetworkMessage);
|
|
18320
18354
|
}
|
|
18355
|
+
function resolveBlossomServers(requestServers, configuredServers) {
|
|
18356
|
+
const servers = [];
|
|
18357
|
+
for (const value of requestServers) {
|
|
18358
|
+
const server = normalizePublicBlossomHint(value);
|
|
18359
|
+
if (server && !servers.includes(server)) servers.push(server);
|
|
18360
|
+
if (servers.length === PAJA_RESOURCE_MAX_SERVERS) return servers;
|
|
18361
|
+
}
|
|
18362
|
+
for (const server of usableBlossomServers(configuredServers)) {
|
|
18363
|
+
if (!servers.includes(server)) servers.push(server);
|
|
18364
|
+
if (servers.length === PAJA_RESOURCE_MAX_SERVERS) break;
|
|
18365
|
+
}
|
|
18366
|
+
return servers;
|
|
18367
|
+
}
|
|
18368
|
+
function normalizePublicBlossomHint(value) {
|
|
18369
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
18370
|
+
try {
|
|
18371
|
+
const url = new URL(value.trim());
|
|
18372
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" || !isPublicHostname(url.hostname)) return null;
|
|
18373
|
+
return url.origin;
|
|
18374
|
+
} catch {
|
|
18375
|
+
return null;
|
|
18376
|
+
}
|
|
18377
|
+
}
|
|
18378
|
+
function isPublicHostname(value) {
|
|
18379
|
+
const hostname = value.replace(/^\[|\]$/gu, "").toLowerCase();
|
|
18380
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname.endsWith(".internal") || !hostname.includes(".")) return false;
|
|
18381
|
+
if (hostname.includes(":")) {
|
|
18382
|
+
return hostname !== "::" && hostname !== "::1" && !hostname.startsWith("::ffff:") && !/^(?:fc|fd|fe[89ab]|ff)/iu.test(hostname) && !hostname.startsWith("2001:db8:");
|
|
18383
|
+
}
|
|
18384
|
+
const octets = hostname.split(".").map(Number);
|
|
18385
|
+
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return true;
|
|
18386
|
+
const [first, second] = octets;
|
|
18387
|
+
return first !== 0 && first !== 10 && first !== 127 && first < 224 && !(first === 100 && second >= 64 && second <= 127) && !(first === 169 && second === 254) && !(first === 172 && second >= 16 && second <= 31) && !(first === 192 && second === 168) && !(first === 198 && (second === 18 || second === 19));
|
|
18388
|
+
}
|
|
18321
18389
|
function usableBlossomServers(values) {
|
|
18322
18390
|
const servers = [];
|
|
18323
18391
|
for (const value of values) {
|
|
@@ -19649,7 +19717,7 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
|
|
|
19649
19717
|
const services = {
|
|
19650
19718
|
resource: createResourceService({
|
|
19651
19719
|
fetch: createPajaResourceFetch({ getBlossomServers }),
|
|
19652
|
-
resourceInfo: () => pajaResourceInfo(
|
|
19720
|
+
resourceInfo: () => pajaResourceInfo()
|
|
19653
19721
|
})
|
|
19654
19722
|
};
|
|
19655
19723
|
if (getSimulation().capabilities.domains.keys && typeof document !== "undefined") {
|
|
@@ -20011,7 +20079,7 @@ function injectPajaRuntimeCsp(html, origins) {
|
|
|
20011
20079
|
const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
|
|
20012
20080
|
const value = [
|
|
20013
20081
|
"default-src 'none'",
|
|
20014
|
-
"script-src 'unsafe-inline'",
|
|
20082
|
+
"script-src 'unsafe-inline' 'wasm-unsafe-eval'",
|
|
20015
20083
|
"style-src 'unsafe-inline'",
|
|
20016
20084
|
"img-src data: blob:",
|
|
20017
20085
|
"font-src data:",
|