@blockcast/mmt-container 1.0.1 → 2.0.0-main.609e08d7e29f
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 +9 -7
- package/dist/browser-runtime.js +6972 -6693
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.d.ts +7 -7
- package/dist/carp-catalog.d.ts.map +1 -1
- package/dist/carp-catalog.js +65 -230
- package/dist/carp-catalog.js.map +3 -3
- package/dist/catalog-fec-params.js.map +1 -1
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/delivery-catalog.js +3 -0
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +25 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +208 -15
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.d.ts +8 -0
- package/dist/held-layer-admission.d.ts.map +1 -1
- package/dist/held-layer-admission.js +22 -1
- package/dist/held-layer-admission.js.map +2 -2
- package/dist/iso-block-locator.js.map +1 -1
- package/dist/mfu-reassembler.js.map +1 -1
- package/dist/mmtp-container-parser.js.map +1 -1
- package/dist/mmtp-container-parser.no-modules.js +146 -332
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js.map +1 -1
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +61 -15
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +11 -6
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +47 -10
- package/dist/mmtp-router.js.map +3 -3
- package/dist/multicast-auth-catalog.d.ts +8 -8
- package/dist/multicast-auth-catalog.d.ts.map +1 -1
- package/dist/multicast-auth-catalog.js +171 -75
- package/dist/multicast-auth-catalog.js.map +4 -4
- package/dist/multicast-input.d.ts +5 -5
- package/dist/multicast-input.d.ts.map +1 -1
- package/dist/multicast-input.js +126 -49
- package/dist/multicast-input.js.map +3 -3
- package/dist/multicast-manager.d.ts +11 -4
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +7914 -7659
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +16 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +120 -5
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +2 -2
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +100 -288
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.js.map +1 -1
- package/dist/signaling.js.map +1 -1
- package/dist/timestamp-codec.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js.map +1 -1
- package/package.json +5 -5
- package/schemas/catalog.schema.json +15 -7
- package/src/carp-catalog.ts +14 -13
- package/src/fec-manager.ts +89 -14
- package/src/held-layer-admission.ts +38 -1
- package/src/mmtp-fec-session.ts +20 -12
- package/src/mmtp-router.ts +65 -16
- package/src/multicast-auth-catalog.ts +43 -36
- package/src/multicast-input.ts +28 -8
- package/src/multicast-manager.ts +38 -34
- package/src/multicast-transport.ts +74 -7
- package/test-vectors/bc-provenance-v2.json +614 -175
- package/test-vectors/catalog/README.md +24 -8
- package/test-vectors/catalog/negative/multicast-auth-asm-missing-sender-address.json +0 -2
- package/test-vectors/catalog/negative/{multicast-auth-manifest-port-equals-port.json → multicast-auth-draft01-manifest-port.json} +1 -1
- package/test-vectors/catalog/negative/multicast-auth-duplicate-first-epoch.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-duplicate-kid.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-endpoint-auth-without-object-auth.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-endpoint-missing-bandwidth.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-manifest-timer-out-of-range.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-sender-address-on-ssm.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-shared-sg-port.json +73 -0
- package/test-vectors/catalog/negative/multicast-auth-version-1.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-version-3.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-version-missing.json +0 -1
- package/test-vectors/catalog/negative/multicast-endpoint-address-family-mismatch.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-host-name.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-leading-zero.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-ls32-leading-zero.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-mapped-family-mismatch.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-zone-id.json +55 -0
- package/test-vectors/catalog/positive/multicast-auth-asm-sender.json +0 -2
- package/test-vectors/catalog/{negative/multicast-auth-max-auth-delay-above-max.json → positive/multicast-auth-max-auth-delay-above-max-clamped.json} +0 -1
- package/test-vectors/catalog/{negative/multicast-auth-max-auth-delay-below-min.json → positive/multicast-auth-max-auth-delay-below-min-clamped.json} +0 -1
- package/test-vectors/catalog/positive/multicast-auth-max-auth-delay-string.json +55 -0
- package/test-vectors/catalog/positive/multicast-auth-rotation.json +0 -1
- package/test-vectors/catalog/positive/multicast-draft01-7-2-6-example.json +0 -1
- package/test-vectors/catalog/section-codes.json +8 -3
- package/test-vectors/catalog-multicast-auth-v1.json +0 -1
- package/test-vectors/catalog-validation.json +3 -3
- package/test-vectors/generate-bc-provenance-v2.mjs +94 -14
package/dist/no-modules.js
CHANGED
|
@@ -319,14 +319,11 @@ function readRepairFecPayloadIdFields(mmtpPacket, parsedHeader) {
|
|
|
319
319
|
dataOffset: off + REPAIR_FEC_PID_SIZE
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
|
-
function
|
|
323
|
-
return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header));
|
|
324
|
-
}
|
|
325
|
-
function moqRepairFrameFrom(frame, repairId) {
|
|
322
|
+
function moqRepairFrameFrom(frame, repairId, checkRsId = true) {
|
|
326
323
|
if (!repairId)
|
|
327
324
|
return null;
|
|
328
325
|
const symbolData = frame.subarray(repairId.dataOffset);
|
|
329
|
-
if (repairId.
|
|
326
|
+
if (repairId.ssbLength === 0 || checkRsId && repairId.rsId >= repairId.rsbLength || symbolData.length === 0)
|
|
330
327
|
return null;
|
|
331
328
|
return {
|
|
332
329
|
packetId: repairId.packetId,
|
|
@@ -337,6 +334,23 @@ function moqRepairFrameFrom(frame, repairId) {
|
|
|
337
334
|
symbolData
|
|
338
335
|
};
|
|
339
336
|
}
|
|
337
|
+
function admitMmtpRepairObject(packet, header, symbolSize, algorithm) {
|
|
338
|
+
if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2)
|
|
339
|
+
return null;
|
|
340
|
+
if (header.headerLength !== MMTP_HEADER_SIZE || packet.length !== MMTP_HEADER_SIZE + REPAIR_FEC_PID_SIZE + symbolSize) {
|
|
341
|
+
return { reject: "repairLength" };
|
|
342
|
+
}
|
|
343
|
+
const frame = moqRepairFrameFrom(packet, readRepairFecPayloadIdFields(packet, header), false);
|
|
344
|
+
if (!frame)
|
|
345
|
+
return null;
|
|
346
|
+
if (frame.rsId >= frame.rsbLength)
|
|
347
|
+
return { frame, reject: "repairRsId" };
|
|
348
|
+
const blockSymbols = frame.ssbLength + frame.rsbLength;
|
|
349
|
+
if (algorithm === "reed-solomon" ? blockSymbols > 255 : algorithm === "raptorq" && blockSymbols - 1 > 16777215) {
|
|
350
|
+
return { frame, reject: "repairSchemeBound" };
|
|
351
|
+
}
|
|
352
|
+
return { frame };
|
|
353
|
+
}
|
|
340
354
|
|
|
341
355
|
// ../mmt-base/dist/net-constants.js
|
|
342
356
|
var DEFAULT_AMT_RELAY_PORT = 2268;
|
|
@@ -414,254 +428,6 @@ function isoBlocksBehindLowerBound(a, b, kMax) {
|
|
|
414
428
|
return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0;
|
|
415
429
|
}
|
|
416
430
|
|
|
417
|
-
// ../mmt-base/dist/multicast-catalog.js
|
|
418
|
-
var ED25519_PUBLIC_KEY_BASE64URL = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
419
|
-
var UINT32_MAX = 4294967295;
|
|
420
|
-
var AUTH_MEMBERS = /* @__PURE__ */ new Set(["scheme", "version", "sig", "keys", "minEpoch"]);
|
|
421
|
-
var KEY_MEMBERS = /* @__PURE__ */ new Set(["kid", "publicKey", "firstEpoch"]);
|
|
422
|
-
var ENDPOINT_AUTH_MEMBERS = /* @__PURE__ */ new Set([
|
|
423
|
-
"manifestPort",
|
|
424
|
-
"streamId",
|
|
425
|
-
"senderAddress",
|
|
426
|
-
"maxAuthDelayMs",
|
|
427
|
-
"manifestTimerMs",
|
|
428
|
-
"manifestTrack"
|
|
429
|
-
]);
|
|
430
|
-
function isObject(value) {
|
|
431
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
432
|
-
}
|
|
433
|
-
function isNonEmptyString(value) {
|
|
434
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
435
|
-
}
|
|
436
|
-
function isIntegerIn(value, min, max) {
|
|
437
|
-
return Number.isSafeInteger(value) && value >= min && value <= max;
|
|
438
|
-
}
|
|
439
|
-
function multicastEndpointSource(endpoint) {
|
|
440
|
-
if (!isObject(endpoint))
|
|
441
|
-
return void 0;
|
|
442
|
-
const sourceAddress = endpoint["sourceAddress"];
|
|
443
|
-
if (sourceAddress !== void 0 && !isNonEmptyString(sourceAddress))
|
|
444
|
-
return void 0;
|
|
445
|
-
const mode = sourceAddress === void 0 ? "asm" : "ssm";
|
|
446
|
-
const protocol = endpoint["protocol"];
|
|
447
|
-
if (protocol !== void 0 && protocol !== mode)
|
|
448
|
-
return void 0;
|
|
449
|
-
const auth = endpoint["auth"];
|
|
450
|
-
const senderAddress = isObject(auth) ? auth["senderAddress"] : void 0;
|
|
451
|
-
if (mode === "ssm")
|
|
452
|
-
return senderAddress === void 0 ? { mode, source: sourceAddress } : void 0;
|
|
453
|
-
if (auth === void 0)
|
|
454
|
-
return { mode };
|
|
455
|
-
return isNonEmptyString(senderAddress) ? { mode, source: senderAddress } : void 0;
|
|
456
|
-
}
|
|
457
|
-
function authIssues(auth) {
|
|
458
|
-
const base = "$.multicast.auth";
|
|
459
|
-
const issue = (path, message) => [
|
|
460
|
-
{ code: "multicast_auth", path: `${base}${path}`, message }
|
|
461
|
-
];
|
|
462
|
-
if (!isObject(auth))
|
|
463
|
-
return issue("", "auth must be an object");
|
|
464
|
-
const unknown = Object.keys(auth).filter((key) => !AUTH_MEMBERS.has(key));
|
|
465
|
-
if (unknown.length > 0) {
|
|
466
|
-
return issue("", `auth has unknown member(s) ${unknown.join(", ")}; draft -01 7.2.6 defines scheme, version, sig, keys and minEpoch`);
|
|
467
|
-
}
|
|
468
|
-
if (auth["scheme"] !== "bc-provenance")
|
|
469
|
-
return issue(".scheme", 'scheme must be "bc-provenance"');
|
|
470
|
-
if (auth["version"] !== 2)
|
|
471
|
-
return issue(".version", "version must be 2 (bc-provenance version 2, draft -01 7.2)");
|
|
472
|
-
if (auth["sig"] !== "ed25519")
|
|
473
|
-
return issue(".sig", 'sig must be "ed25519"');
|
|
474
|
-
const keys = auth["keys"];
|
|
475
|
-
if (!Array.isArray(keys) || keys.length < 1 || keys.length > 4) {
|
|
476
|
-
return issue(".keys", "keys must be an array of one to four {kid, publicKey, firstEpoch} objects");
|
|
477
|
-
}
|
|
478
|
-
const kids = /* @__PURE__ */ new Set();
|
|
479
|
-
const firstEpochs = /* @__PURE__ */ new Set();
|
|
480
|
-
for (let index = 0; index < keys.length; index++) {
|
|
481
|
-
const key = keys[index];
|
|
482
|
-
const at = `.keys[${index}]`;
|
|
483
|
-
if (!isObject(key) || Object.keys(key).some((member) => !KEY_MEMBERS.has(member))) {
|
|
484
|
-
return issue(at, "a key is exactly {kid, publicKey, firstEpoch}");
|
|
485
|
-
}
|
|
486
|
-
if (!isIntegerIn(key["kid"], 0, UINT32_MAX))
|
|
487
|
-
return issue(`${at}.kid`, "kid must be an unsigned 32-bit integer");
|
|
488
|
-
if (typeof key["publicKey"] !== "string" || !ED25519_PUBLIC_KEY_BASE64URL.test(key["publicKey"])) {
|
|
489
|
-
return issue(`${at}.publicKey`, "publicKey must be a 32-octet Ed25519 key in canonical unpadded base64url");
|
|
490
|
-
}
|
|
491
|
-
if (!isIntegerIn(key["firstEpoch"], 0, UINT32_MAX)) {
|
|
492
|
-
return issue(`${at}.firstEpoch`, "firstEpoch must be an unsigned 32-bit integer");
|
|
493
|
-
}
|
|
494
|
-
if (kids.has(key["kid"]))
|
|
495
|
-
return issue(`${at}.kid`, `kid ${key["kid"]} is repeated; the catalog holds no key set`);
|
|
496
|
-
if (firstEpochs.has(key["firstEpoch"])) {
|
|
497
|
-
return issue(`${at}.firstEpoch`, `firstEpoch ${key["firstEpoch"]} is repeated; the catalog holds no key set`);
|
|
498
|
-
}
|
|
499
|
-
kids.add(key["kid"]);
|
|
500
|
-
firstEpochs.add(key["firstEpoch"]);
|
|
501
|
-
}
|
|
502
|
-
if (!isIntegerIn(auth["minEpoch"], 0, UINT32_MAX)) {
|
|
503
|
-
return issue(".minEpoch", "minEpoch must be an unsigned 32-bit integer");
|
|
504
|
-
}
|
|
505
|
-
return [];
|
|
506
|
-
}
|
|
507
|
-
function endpointAuthIssue(endpoint, mode, at) {
|
|
508
|
-
const auth = endpoint["auth"];
|
|
509
|
-
if (!isObject(auth))
|
|
510
|
-
return { path: `${at}.auth`, message: "auth must be an object" };
|
|
511
|
-
const unknown = Object.keys(auth).filter((key) => !ENDPOINT_AUTH_MEMBERS.has(key));
|
|
512
|
-
if (unknown.length > 0)
|
|
513
|
-
return { path: `${at}.auth`, message: `auth has unknown member(s) ${unknown.join(", ")}` };
|
|
514
|
-
if (!isIntegerIn(auth["manifestPort"], 1, 65535)) {
|
|
515
|
-
return { path: `${at}.auth.manifestPort`, message: "manifestPort must be an integer from 1 to 65535" };
|
|
516
|
-
}
|
|
517
|
-
if (!isIntegerIn(auth["streamId"], 0, UINT32_MAX)) {
|
|
518
|
-
return { path: `${at}.auth.streamId`, message: "streamId must be an unsigned 32-bit integer" };
|
|
519
|
-
}
|
|
520
|
-
if (mode === "ssm" && auth["senderAddress"] !== void 0) {
|
|
521
|
-
return { path: `${at}.auth.senderAddress`, message: "senderAddress must not appear on an SSM endpoint; S is sourceAddress" };
|
|
522
|
-
}
|
|
523
|
-
if (mode === "asm" && !isNonEmptyString(auth["senderAddress"])) {
|
|
524
|
-
return { path: `${at}.auth.senderAddress`, message: "a protected ASM endpoint requires senderAddress" };
|
|
525
|
-
}
|
|
526
|
-
if (auth["maxAuthDelayMs"] !== void 0 && !isIntegerIn(auth["maxAuthDelayMs"], 50, 1e3)) {
|
|
527
|
-
return { path: `${at}.auth.maxAuthDelayMs`, message: "maxAuthDelayMs must be an integer from 50 to 1000" };
|
|
528
|
-
}
|
|
529
|
-
if (auth["manifestTimerMs"] !== void 0 && !isIntegerIn(auth["manifestTimerMs"], 1, Number.MAX_SAFE_INTEGER)) {
|
|
530
|
-
return { path: `${at}.auth.manifestTimerMs`, message: "manifestTimerMs must be an integer of at least 1" };
|
|
531
|
-
}
|
|
532
|
-
if (auth["manifestTrack"] !== void 0 && !isNonEmptyString(auth["manifestTrack"])) {
|
|
533
|
-
return { path: `${at}.auth.manifestTrack`, message: "manifestTrack must be a non-empty string" };
|
|
534
|
-
}
|
|
535
|
-
if (!isIntegerIn(endpoint["bandwidth"], 1, Number.MAX_SAFE_INTEGER)) {
|
|
536
|
-
return { path: `${at}.bandwidth`, message: "a protected endpoint requires bandwidth, which bounds the receiver's hold queues" };
|
|
537
|
-
}
|
|
538
|
-
return void 0;
|
|
539
|
-
}
|
|
540
|
-
var CATALOG_NAMESPACE = /* @__PURE__ */ Symbol.for("@blockcast/mmt-base/catalogNamespace");
|
|
541
|
-
function multicastCatalogIssues(catalog, options = {}) {
|
|
542
|
-
if (!isObject(catalog))
|
|
543
|
-
return [];
|
|
544
|
-
const issues = [];
|
|
545
|
-
if ("multicast.auth" in catalog) {
|
|
546
|
-
issues.push({
|
|
547
|
-
code: "multicast_auth",
|
|
548
|
-
path: '$["multicast.auth"]',
|
|
549
|
-
message: 'the root "multicast.auth" key is retired; declare auth as multicast.auth (draft -01 7.2.6)'
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
const multicast = catalog["multicast"];
|
|
553
|
-
if (!isObject(multicast))
|
|
554
|
-
return issues;
|
|
555
|
-
const protectedCatalog = multicast["auth"] !== void 0;
|
|
556
|
-
if (protectedCatalog)
|
|
557
|
-
issues.push(...authIssues(multicast["auth"]));
|
|
558
|
-
const endpoints = multicast["endpoints"];
|
|
559
|
-
if (!Array.isArray(endpoints))
|
|
560
|
-
return issues;
|
|
561
|
-
const tracks = Array.isArray(catalog["tracks"]) ? catalog["tracks"] : [];
|
|
562
|
-
const namespaceOf = /* @__PURE__ */ new Map();
|
|
563
|
-
for (const track of tracks) {
|
|
564
|
-
if (isObject(track) && typeof track["name"] === "string")
|
|
565
|
-
namespaceOf.set(track["name"], track["namespace"]);
|
|
566
|
-
}
|
|
567
|
-
const tagged = catalog[CATALOG_NAMESPACE];
|
|
568
|
-
let referenceNamespace = options.catalogNamespace ?? (typeof tagged === "string" ? tagged : void 0);
|
|
569
|
-
const sources = [];
|
|
570
|
-
for (let index = 0; index < endpoints.length; index++) {
|
|
571
|
-
const endpoint = endpoints[index];
|
|
572
|
-
const at = `$.multicast.endpoints[${index}]`;
|
|
573
|
-
const fail = (code, path, message) => {
|
|
574
|
-
issues.push({ code, path, message, endpointIndex: index });
|
|
575
|
-
};
|
|
576
|
-
sources.push(void 0);
|
|
577
|
-
if (!isObject(endpoint)) {
|
|
578
|
-
fail("multicast_endpoint", at, "endpoint must be an object");
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
const protocol = endpoint["protocol"];
|
|
582
|
-
if (protocol !== void 0 && protocol !== "ssm" && protocol !== "asm") {
|
|
583
|
-
fail("multicast_endpoint", `${at}.protocol`, `protocol must be "ssm" or "asm"; a bearer such as ${JSON.stringify(protocol)} is a networkSource entry (draft -01 4.2)`);
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
const hasAuth = endpoint["auth"] !== void 0;
|
|
587
|
-
if (protectedCatalog !== hasAuth) {
|
|
588
|
-
fail("multicast_endpoint_auth", `${at}.auth`, protectedCatalog ? "every endpoint requires auth when multicast carries auth; a receiver ignores this endpoint" : "auth names no key without multicast.auth; a receiver ignores this endpoint");
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
const sourceAddress = endpoint["sourceAddress"];
|
|
592
|
-
if (sourceAddress !== void 0 && !isNonEmptyString(sourceAddress)) {
|
|
593
|
-
fail("multicast_endpoint", `${at}.sourceAddress`, "sourceAddress must be a non-empty string");
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
const mode = sourceAddress === void 0 ? "asm" : "ssm";
|
|
597
|
-
if (protocol !== void 0 && protocol !== mode) {
|
|
598
|
-
fail("multicast_endpoint", `${at}.protocol`, `protocol ${JSON.stringify(protocol)} disagrees with sourceAddress, which selects ${mode}`);
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (!isIntegerIn(endpoint["port"], 1, 65535)) {
|
|
602
|
-
fail("multicast_endpoint", `${at}.port`, "port must be an integer from 1 to 65535");
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
if (hasAuth) {
|
|
606
|
-
const authIssue = endpointAuthIssue(endpoint, mode, at);
|
|
607
|
-
if (authIssue) {
|
|
608
|
-
fail("multicast_endpoint_auth", authIssue.path, authIssue.message);
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
sources[index] = multicastEndpointSource(endpoint);
|
|
613
|
-
const refs = Array.isArray(endpoint["tracks"]) ? endpoint["tracks"] : [];
|
|
614
|
-
for (let refIndex = 0; refIndex < refs.length; refIndex++) {
|
|
615
|
-
const ref = refs[refIndex];
|
|
616
|
-
const name = isObject(ref) ? ref["name"] : ref;
|
|
617
|
-
if (typeof name !== "string")
|
|
618
|
-
continue;
|
|
619
|
-
const namespace = namespaceOf.get(name);
|
|
620
|
-
if (typeof namespace !== "string")
|
|
621
|
-
continue;
|
|
622
|
-
referenceNamespace ??= namespace;
|
|
623
|
-
if (namespace !== referenceNamespace) {
|
|
624
|
-
fail("multicast_track", `${at}.tracks[${refIndex}].name`, `track ${JSON.stringify(name)} declares namespace ${JSON.stringify(namespace)}, not the catalog track's ${JSON.stringify(referenceNamespace)}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
const tupleOf = (index) => {
|
|
629
|
-
const endpoint = endpoints[index];
|
|
630
|
-
const auth = endpoint["auth"];
|
|
631
|
-
return {
|
|
632
|
-
sg: JSON.stringify([sources[index].source ?? null, endpoint["groupAddress"]]),
|
|
633
|
-
port: endpoint["port"],
|
|
634
|
-
manifestPort: isObject(auth) ? auth["manifestPort"] : void 0
|
|
635
|
-
};
|
|
636
|
-
};
|
|
637
|
-
for (let index = 0; index < endpoints.length; index++) {
|
|
638
|
-
if (!sources[index])
|
|
639
|
-
continue;
|
|
640
|
-
const own = tupleOf(index);
|
|
641
|
-
const at = `$.multicast.endpoints[${index}]`;
|
|
642
|
-
if (own.manifestPort !== void 0 && own.manifestPort === own.port) {
|
|
643
|
-
issues.push({ code: "multicast_endpoint_auth", path: `${at}.auth.manifestPort`, message: "manifestPort must differ from the endpoint's own port", endpointIndex: index });
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
for (let other = 0; other < endpoints.length; other++) {
|
|
647
|
-
if (other === index || !sources[other])
|
|
648
|
-
continue;
|
|
649
|
-
const peer = tupleOf(other);
|
|
650
|
-
if (peer.sg !== own.sg)
|
|
651
|
-
continue;
|
|
652
|
-
if (peer.port === own.port) {
|
|
653
|
-
issues.push({ code: "multicast_endpoint", path: `${at}.port`, message: `(S, G, port) is shared with endpoint ${other}; one endpoint object per multicast tuple`, endpointIndex: index });
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
if (own.manifestPort !== void 0 && (own.manifestPort === peer.port || own.manifestPort === peer.manifestPort)) {
|
|
657
|
-
issues.push({ code: "multicast_endpoint_auth", path: `${at}.auth.manifestPort`, message: `manifestPort is a port or manifestPort of endpoint ${other} on the same (S, G)`, endpointIndex: index });
|
|
658
|
-
break;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
return issues;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
431
|
// ../mmt-base/dist/fec-catalog.js
|
|
666
432
|
var DEFAULT_SCHEMES = ["reed-solomon", "raptorq"];
|
|
667
433
|
var DEFAULT_PROFILES = ["moq-object-v1", "iso-ssbg1-v1"];
|
|
@@ -7962,7 +7728,7 @@ function validateNetworkSourceRelays(sources, path) {
|
|
|
7962
7728
|
}
|
|
7963
7729
|
});
|
|
7964
7730
|
}
|
|
7965
|
-
function validateCmsfMmtpCatalog(value,
|
|
7731
|
+
function validateCmsfMmtpCatalog(value, _options = {}) {
|
|
7966
7732
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
7967
7733
|
catalogError("catalog_object", "$", "catalog must be an object");
|
|
7968
7734
|
}
|
|
@@ -8153,8 +7919,6 @@ function validateCmsfMmtpCatalog(value, options = {}) {
|
|
|
8153
7919
|
}
|
|
8154
7920
|
}
|
|
8155
7921
|
}
|
|
8156
|
-
const [multicastIssue] = multicastCatalogIssues(catalog, options);
|
|
8157
|
-
if (multicastIssue) catalogError(multicastIssue.code, multicastIssue.path, multicastIssue.message);
|
|
8158
7922
|
validateArchive(catalog);
|
|
8159
7923
|
return catalog;
|
|
8160
7924
|
}
|
|
@@ -8233,6 +7997,9 @@ var HeldLayerAdmission = class {
|
|
|
8233
7997
|
* @returns undefined to admit, `'discard'`, or the fail-closed reason.
|
|
8234
7998
|
*/
|
|
8235
7999
|
admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier) {
|
|
8000
|
+
return this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, true);
|
|
8001
|
+
}
|
|
8002
|
+
#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, retroactive) {
|
|
8236
8003
|
if (repairTrackName !== void 0 && !this.#layerNames.has(repairTrackName)) return "discard";
|
|
8237
8004
|
const farAhead = sourceFrontier !== void 0 && serial32Precedes(sourceFrontier, ssStart) && serial32Distance(sourceFrontier, ssStart) > horizon;
|
|
8238
8005
|
for (const state of this.#states) {
|
|
@@ -8260,7 +8027,7 @@ var HeldLayerAdmission = class {
|
|
|
8260
8027
|
}
|
|
8261
8028
|
} else if (!state.settled && !stale(ssStart)) {
|
|
8262
8029
|
if (state.anchor === void 0 || serial32Precedes(ssStart, state.anchor)) state.anchor = ssStart;
|
|
8263
|
-
if (state.latestBelow !== void 0 && !serial32Precedes(state.latestBelow, state.anchor)) {
|
|
8030
|
+
if (retroactive && state.latestBelow !== void 0 && !serial32Precedes(state.latestBelow, state.anchor)) {
|
|
8264
8031
|
code = "below_anchored_total";
|
|
8265
8032
|
ssStart = state.latestBelow;
|
|
8266
8033
|
rsbLength = state.latestBelowRsb;
|
|
@@ -8280,6 +8047,24 @@ var HeldLayerAdmission = class {
|
|
|
8280
8047
|
}
|
|
8281
8048
|
return void 0;
|
|
8282
8049
|
}
|
|
8050
|
+
/**
|
|
8051
|
+
* The core-00 7.5 carve-out for a repair object about to be discarded
|
|
8052
|
+
* (RS_ID or scheme bound): whether its own RSB_length, below a held layer's
|
|
8053
|
+
* total on that layer's track or on an anchored block, fails the instance
|
|
8054
|
+
* closed under 7.3.2. Leaves the anchored state as it was: a discarded
|
|
8055
|
+
* object anchors nothing.
|
|
8056
|
+
*/
|
|
8057
|
+
failsClosed(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier) {
|
|
8058
|
+
const saved = this.#states.map((state) => ({ ...state }));
|
|
8059
|
+
try {
|
|
8060
|
+
const verdict = this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, false);
|
|
8061
|
+
return typeof verdict === "object" ? verdict : void 0;
|
|
8062
|
+
} finally {
|
|
8063
|
+
saved.forEach((state, i) => {
|
|
8064
|
+
this.#states[i] = state;
|
|
8065
|
+
});
|
|
8066
|
+
}
|
|
8067
|
+
}
|
|
8283
8068
|
};
|
|
8284
8069
|
|
|
8285
8070
|
// src/mmtp-fec-session.ts
|
|
@@ -8450,18 +8235,29 @@ var MmtpFecSession = class {
|
|
|
8450
8235
|
} catch {
|
|
8451
8236
|
return this.#drop("repair:malformed");
|
|
8452
8237
|
}
|
|
8453
|
-
const
|
|
8454
|
-
if (!
|
|
8455
|
-
if (
|
|
8456
|
-
|
|
8457
|
-
|
|
8238
|
+
const admission = admitMmtpRepairObject(packet, header, this.symbolSize, this.algorithm);
|
|
8239
|
+
if (!admission) return this.#drop("repair:malformed");
|
|
8240
|
+
if (!("frame" in admission)) return this.#drop("repair:length");
|
|
8241
|
+
const frame = admission.frame;
|
|
8242
|
+
if (this.#repairPacketId !== void 0 && frame.packetId !== this.#repairPacketId) {
|
|
8458
8243
|
return this.#drop("repair:foreign-packet-id");
|
|
8459
8244
|
}
|
|
8460
|
-
if (
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8245
|
+
if (admission.reject) {
|
|
8246
|
+
const verdict2 = this.#heldLayers.failsClosed(
|
|
8247
|
+
repairTrackName,
|
|
8248
|
+
frame.ssStart,
|
|
8249
|
+
frame.rsbLength,
|
|
8250
|
+
this.#anchorHorizon,
|
|
8251
|
+
this.#newestSource
|
|
8252
|
+
);
|
|
8253
|
+
if (verdict2) {
|
|
8254
|
+
this.#stats.failClosed = verdict2;
|
|
8255
|
+
return this.#drop("repair:fail-closed");
|
|
8256
|
+
}
|
|
8257
|
+
return this.#drop(admission.reject === "repairRsId" ? "repair:rs-id" : "repair:scheme-bound");
|
|
8464
8258
|
}
|
|
8259
|
+
this.#repairPacketId ??= frame.packetId;
|
|
8260
|
+
if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
|
|
8465
8261
|
const verdict = this.#heldLayers.admit(
|
|
8466
8262
|
repairTrackName,
|
|
8467
8263
|
frame.ssStart,
|
|
@@ -8736,8 +8532,8 @@ function u32(v) {
|
|
|
8736
8532
|
}
|
|
8737
8533
|
var KEYFRAME_FLAGS = 33554432;
|
|
8738
8534
|
var DELTA_FLAGS = 16842752;
|
|
8739
|
-
var
|
|
8740
|
-
var MAX_TRACK_ID =
|
|
8535
|
+
var UINT32_MAX = 4294967295;
|
|
8536
|
+
var MAX_TRACK_ID = UINT32_MAX - 1;
|
|
8741
8537
|
var INT32_MIN = -2147483648;
|
|
8742
8538
|
var INT32_MAX = 2147483647;
|
|
8743
8539
|
function assertTrackId(trackId, label) {
|
|
@@ -8746,8 +8542,8 @@ function assertTrackId(trackId, label) {
|
|
|
8746
8542
|
}
|
|
8747
8543
|
}
|
|
8748
8544
|
function assertTimescale(timescale, label) {
|
|
8749
|
-
if (!Number.isInteger(timescale) || timescale <= 0 || timescale >
|
|
8750
|
-
throw new RangeError(`${label} must be an integer in 1..${
|
|
8545
|
+
if (!Number.isInteger(timescale) || timescale <= 0 || timescale > UINT32_MAX) {
|
|
8546
|
+
throw new RangeError(`${label} must be an integer in 1..${UINT32_MAX}, got ${timescale}`);
|
|
8751
8547
|
}
|
|
8752
8548
|
}
|
|
8753
8549
|
function assertCompositionTimeOffsets(samples) {
|
|
@@ -10060,6 +9856,11 @@ function parseIpv6(address) {
|
|
|
10060
9856
|
const [leftText, rightText = ""] = address.split("::");
|
|
10061
9857
|
const left = leftText ? leftText.split(":") : [];
|
|
10062
9858
|
const right = rightText ? rightText.split(":") : [];
|
|
9859
|
+
const tail = compressed ? right : left;
|
|
9860
|
+
if (tail.length > 0 && tail[tail.length - 1].includes(".")) {
|
|
9861
|
+
const v4 = parseIpv4(tail.pop());
|
|
9862
|
+
tail.push((v4[0] << 8 | v4[1]).toString(16), (v4[2] << 8 | v4[3]).toString(16));
|
|
9863
|
+
}
|
|
10063
9864
|
if (left.some((group) => group.includes(".")) || right.some((group) => group.includes("."))) {
|
|
10064
9865
|
throw new Error(`invalid IPv6 address ${address}`);
|
|
10065
9866
|
}
|
|
@@ -10082,7 +9883,7 @@ function parseBcProvenanceV2Address(address) {
|
|
|
10082
9883
|
return parseIpv4(address);
|
|
10083
9884
|
}
|
|
10084
9885
|
function endpointBytes(endpoint) {
|
|
10085
|
-
if (!validPort(endpoint.port) || !
|
|
9886
|
+
if (!validPort(endpoint.port) || !validU32(endpoint.streamId)) {
|
|
10086
9887
|
throw new Error("invalid BCPV2 endpoint");
|
|
10087
9888
|
}
|
|
10088
9889
|
const source = parseBcProvenanceV2Address(endpoint.sourceAddress);
|
|
@@ -10142,7 +9943,7 @@ function bcProvenanceV2SignedMessage(record, endpoint) {
|
|
|
10142
9943
|
offset += source.byteLength;
|
|
10143
9944
|
prefix.set(group, offset);
|
|
10144
9945
|
offset += group.byteLength;
|
|
10145
|
-
prefix.set(u16(endpoint.
|
|
9946
|
+
prefix.set(u16(endpoint.port), offset);
|
|
10146
9947
|
const message = new Uint8Array(prefix.byteLength + record.body.byteLength);
|
|
10147
9948
|
message.set(prefix);
|
|
10148
9949
|
message.set(record.body, prefix.byteLength);
|
|
@@ -11653,7 +11454,6 @@ _RistrettoPoint.Fn = /* @__PURE__ */ (() => Fn)();
|
|
|
11653
11454
|
var BCPV2_LEAD_BOUND_MS = 5e3;
|
|
11654
11455
|
var REPLAY_WINDOW = 64n;
|
|
11655
11456
|
var BUCKET_DEPTH = 64;
|
|
11656
|
-
var BUCKET_RATE_PER_MS = 1;
|
|
11657
11457
|
var hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11658
11458
|
function sameBytes(a, b) {
|
|
11659
11459
|
if (a.byteLength !== b.byteLength)
|
|
@@ -11682,6 +11482,10 @@ var BcProvenanceV2Receiver = class {
|
|
|
11682
11482
|
verifySig;
|
|
11683
11483
|
accepted;
|
|
11684
11484
|
queueLimit;
|
|
11485
|
+
/** Octets a held datagram is charged beyond its payload: the IPv4 or IPv6 plus UDP headers (step 2). */
|
|
11486
|
+
headerOctets;
|
|
11487
|
+
/** Budget refill, tokens per ms: V / 1000 (step 1). */
|
|
11488
|
+
bucketRatePerMs;
|
|
11685
11489
|
current = null;
|
|
11686
11490
|
previous = null;
|
|
11687
11491
|
graceStart = {};
|
|
@@ -11697,10 +11501,12 @@ var BcProvenanceV2Receiver = class {
|
|
|
11697
11501
|
order = 0;
|
|
11698
11502
|
buckets;
|
|
11699
11503
|
constructor(cfg) {
|
|
11700
|
-
|
|
11701
|
-
this.
|
|
11702
|
-
|
|
11703
|
-
|
|
11504
|
+
const delay = cfg.maxAuthDelayMs;
|
|
11505
|
+
this.maxAuthDelayMs = typeof delay !== "number" || !Number.isFinite(delay) ? 200 : delay < 50 ? 50 : delay > 1e3 ? 1e3 : Number.isInteger(delay) ? delay : 200;
|
|
11506
|
+
const timer = cfg.manifestTimerMs;
|
|
11507
|
+
this.manifestTimerMs = typeof timer !== "number" || !Number.isFinite(timer) ? 20 : timer < 1 || Number.isInteger(timer) ? timer : 20;
|
|
11508
|
+
if (!(this.manifestTimerMs >= 1)) {
|
|
11509
|
+
throw new Error("BCPV2 endpoint ignored: manifestTimerMs below 1");
|
|
11704
11510
|
}
|
|
11705
11511
|
if (!(cfg.bandwidth > 0))
|
|
11706
11512
|
throw new Error("BCPV2 endpoint ignored: protected endpoint without bandwidth");
|
|
@@ -11725,6 +11531,8 @@ var BcProvenanceV2Receiver = class {
|
|
|
11725
11531
|
multicast: this.maxAuthDelayMs * bandwidth / 8e3,
|
|
11726
11532
|
unicast: (this.maxAuthDelayMs + this.widenMs) * bandwidth / 8e3
|
|
11727
11533
|
};
|
|
11534
|
+
this.headerOctets = parseBcProvenanceV2Address(cfg.endpoint.groupAddress).byteLength === 16 ? 48 : 28;
|
|
11535
|
+
this.bucketRatePerMs = Math.max(1e3, 1.25 * bandwidth / (8 * 512 * 37)) / 1e3;
|
|
11728
11536
|
const t = this.now();
|
|
11729
11537
|
this.buckets = {
|
|
11730
11538
|
multicast: { tokens: BUCKET_DEPTH, last: t, dropped: 0 },
|
|
@@ -11740,7 +11548,7 @@ var BcProvenanceV2Receiver = class {
|
|
|
11740
11548
|
return 0;
|
|
11741
11549
|
return this.lead ?? BCPV2_LEAD_BOUND_MS;
|
|
11742
11550
|
}
|
|
11743
|
-
/** Step 1: a record datagram on `
|
|
11551
|
+
/** Step 1: a record datagram on `port`, or a record object of the manifest track. */
|
|
11744
11552
|
onRecord(bytes, path) {
|
|
11745
11553
|
const now = this.now();
|
|
11746
11554
|
this.expire(now);
|
|
@@ -11823,7 +11631,7 @@ var BcProvenanceV2Receiver = class {
|
|
|
11823
11631
|
arrival: now,
|
|
11824
11632
|
sent,
|
|
11825
11633
|
digest: hex(sha2562(bytes)),
|
|
11826
|
-
pairs: record.digests.map((d, i) => ({ digest: hex(d), seq: record.firstSeq + BigInt(i), admitted: false })),
|
|
11634
|
+
pairs: record.digests.map((d, i) => ({ digest: hex(d), epoch, seq: record.firstSeq + BigInt(i), admitted: false })),
|
|
11827
11635
|
awaitingMulticast: path === "mirror"
|
|
11828
11636
|
};
|
|
11829
11637
|
this.verified.set(id, entry);
|
|
@@ -11890,12 +11698,12 @@ var BcProvenanceV2Receiver = class {
|
|
|
11890
11698
|
digest(datagram) {
|
|
11891
11699
|
return hex(bcProvenanceV2DatagramDigest(this.cfg.endpoint, datagram));
|
|
11892
11700
|
}
|
|
11893
|
-
/** Consume the
|
|
11701
|
+
/** Consume the pending entry for `digest` lowest in (epoch, sequence) order, admitting it. */
|
|
11894
11702
|
consumePending(digest, now) {
|
|
11895
11703
|
const list = this.pending.get(digest);
|
|
11896
11704
|
if (!list?.length)
|
|
11897
11705
|
return false;
|
|
11898
|
-
list.sort((a, b) => a.pair.seq < b.pair.seq ? -1 : a.pair.seq > b.pair.seq ? 1 : 0);
|
|
11706
|
+
list.sort((a, b) => a.pair.epoch - b.pair.epoch || (a.pair.seq < b.pair.seq ? -1 : a.pair.seq > b.pair.seq ? 1 : 0));
|
|
11899
11707
|
const hit = list.shift();
|
|
11900
11708
|
if (list.length === 0)
|
|
11901
11709
|
this.pending.delete(digest);
|
|
@@ -11904,14 +11712,18 @@ var BcProvenanceV2Receiver = class {
|
|
|
11904
11712
|
this.admittedUntil.set(digest, now + this.maxAuthDelayMs + this.leadMs);
|
|
11905
11713
|
return true;
|
|
11906
11714
|
}
|
|
11715
|
+
/** A held datagram's charge against its queue: payload plus IP and UDP headers. */
|
|
11716
|
+
charge(held) {
|
|
11717
|
+
return held.datagram.byteLength + this.headerOctets;
|
|
11718
|
+
}
|
|
11907
11719
|
enqueue(held, path, now) {
|
|
11908
|
-
if (this.holdBytes[path] + held
|
|
11720
|
+
if (this.holdBytes[path] + this.charge(held) > this.queueLimit[path]) {
|
|
11909
11721
|
return { verdict: "overflow", admitted: [] };
|
|
11910
11722
|
}
|
|
11911
11723
|
held.expiry = now + this.maxAuthDelayMs + (path === "unicast" ? this.widenMs : 0);
|
|
11912
11724
|
held.order = this.order++;
|
|
11913
11725
|
this.hold[path].push(held);
|
|
11914
|
-
this.holdBytes[path] += held
|
|
11726
|
+
this.holdBytes[path] += this.charge(held);
|
|
11915
11727
|
return { verdict: "held", admitted: [] };
|
|
11916
11728
|
}
|
|
11917
11729
|
/** Remove and return the oldest held datagram with `digest`, across both queues. */
|
|
@@ -11926,7 +11738,7 @@ var BcProvenanceV2Receiver = class {
|
|
|
11926
11738
|
if (!best)
|
|
11927
11739
|
return void 0;
|
|
11928
11740
|
const [held] = this.hold[best.path].splice(best.index, 1);
|
|
11929
|
-
this.holdBytes[best.path] -= held
|
|
11741
|
+
this.holdBytes[best.path] -= this.charge(held);
|
|
11930
11742
|
return held.datagram;
|
|
11931
11743
|
}
|
|
11932
11744
|
dropAdmittedHeld() {
|
|
@@ -11934,7 +11746,7 @@ var BcProvenanceV2Receiver = class {
|
|
|
11934
11746
|
this.hold[path] = this.hold[path].filter((h) => {
|
|
11935
11747
|
const keep = h.digest === void 0 || !this.admittedUntil.has(h.digest);
|
|
11936
11748
|
if (!keep)
|
|
11937
|
-
this.holdBytes[path] -= h
|
|
11749
|
+
this.holdBytes[path] -= this.charge(h);
|
|
11938
11750
|
return keep;
|
|
11939
11751
|
});
|
|
11940
11752
|
}
|
|
@@ -12018,7 +11830,7 @@ var BcProvenanceV2Receiver = class {
|
|
|
12018
11830
|
}
|
|
12019
11831
|
take(path, now) {
|
|
12020
11832
|
const b = this.buckets[path];
|
|
12021
|
-
b.tokens = Math.min(BUCKET_DEPTH, b.tokens + (now - b.last) *
|
|
11833
|
+
b.tokens = Math.min(BUCKET_DEPTH, b.tokens + (now - b.last) * this.bucketRatePerMs);
|
|
12022
11834
|
b.last = now;
|
|
12023
11835
|
if (b.tokens < 1) {
|
|
12024
11836
|
b.dropped++;
|
|
@@ -12032,7 +11844,7 @@ var BcProvenanceV2Receiver = class {
|
|
|
12032
11844
|
this.hold[path] = this.hold[path].filter((h) => {
|
|
12033
11845
|
if (h.expiry > now)
|
|
12034
11846
|
return true;
|
|
12035
|
-
this.holdBytes[path] -= h
|
|
11847
|
+
this.holdBytes[path] -= this.charge(h);
|
|
12036
11848
|
this.expired[path]++;
|
|
12037
11849
|
return false;
|
|
12038
11850
|
});
|
|
@@ -12064,7 +11876,7 @@ var BcProvenanceV2Receiver = class {
|
|
|
12064
11876
|
}
|
|
12065
11877
|
/**
|
|
12066
11878
|
* Retire verified entries below their arrival path's replay floor, keeping a mirrored entry
|
|
12067
|
-
* while the freshness check would still pass its copy on `
|
|
11879
|
+
* while the freshness check would still pass its copy on `port`.
|
|
12068
11880
|
* ponytail: linear scan per record; index by (kid, epoch) if retained sets grow past a few thousand.
|
|
12069
11881
|
*/
|
|
12070
11882
|
prune(now) {
|