@meantjs/format 0.0.0-bootstrap → 0.1.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.
@@ -0,0 +1,49 @@
1
+ // `subaccount` (spec §5.3): a 32-byte blob.
2
+ //
3
+ // input: "#n" or a plain decimal n, 0 <= n < 2^64 (32 bytes, big-endian), or exactly 64 hex
4
+ // characters with an optional 0x prefix (0X is accepted too)
5
+ // display: "#n" if and only if the first 24 bytes are zero, else 64 lowercase hex characters
6
+ //
7
+ // A 64-character string of decimal digits fits both input forms. It is read as hex, because a
8
+ // decimal n < 2^64 has at most 20 digits and a longer decimal is out of range anyway. Decimal
9
+ // numbers with leading zeros are rejected so that no decimal string can be mistaken for hex.
10
+
11
+ import { CODES, fail } from "./errors.mjs";
12
+ import { bytesToHex, hexToBytes, requireString, toBytes } from "./util.mjs";
13
+
14
+ const TWO_64 = 2n ** 64n;
15
+ const HEX64 = /^(?:0[xX])?[0-9a-fA-F]{64}$/;
16
+ const DECIMAL = /^#?(0|[1-9][0-9]*)$/;
17
+ const HEXISH = /^(?:0[xX])?[0-9a-fA-F]+$/;
18
+
19
+ /** @returns {Uint8Array} 32 bytes */
20
+ export function parseSubaccount(input) {
21
+ const text = requireString(input, "subaccount").trim();
22
+ if (HEX64.test(text)) return hexToBytes(text.replace(/^0[xX]/, ""));
23
+ const d = DECIMAL.exec(text);
24
+ if (d) {
25
+ const n = BigInt(d[1]);
26
+ if (n >= TWO_64) fail(CODES.RANGE, `the subaccount number ${d[1]} must be below 2^64 (18446744073709551616); use 64 hex characters for larger values`);
27
+ const out = new Uint8Array(32);
28
+ new DataView(out.buffer).setBigUint64(24, n, false);
29
+ return out;
30
+ }
31
+ const hint = `write "#n" or a plain decimal n with 0 <= n < 2^64, or exactly 64 hex characters (optionally after 0x)`;
32
+ if (/^#?0[0-9]+$/.test(text)) fail(CODES.SYNTAX, `${JSON.stringify(text)} has leading zeros, which would make it ambiguous with hex; ${hint}`);
33
+ if (HEXISH.test(text)) {
34
+ const digits = text.replace(/^0[xX]/, "").length;
35
+ fail(CODES.LENGTH, `${JSON.stringify(text)} has ${digits} hex characters; a hex subaccount has exactly 64. ${hint}`);
36
+ }
37
+ return fail(CODES.SYNTAX, `cannot read ${JSON.stringify(text)} as a subaccount; ${hint}`);
38
+ }
39
+
40
+ /**
41
+ * @param {Uint8Array | number[]} value 32 bytes
42
+ * @returns {string} "#n" when the first 24 bytes are zero, else 64 lowercase hex characters
43
+ */
44
+ export function displaySubaccount(value) {
45
+ const b = toBytes(value, "subaccount");
46
+ if (b.length !== 32) fail(CODES.LENGTH, `a subaccount is 32 bytes, got ${b.length}`);
47
+ if (b.subarray(0, 24).every((x) => x === 0)) return `#${new DataView(b.buffer, b.byteOffset, 32).getBigUint64(24, false)}`;
48
+ return bytesToHex(b);
49
+ }
package/src/time.mjs ADDED
@@ -0,0 +1,258 @@
1
+ // `timestamp` and `duration` (spec §5.3).
2
+ //
3
+ // A timestamp is an integer count of `unit` since the Unix epoch; a duration is an integer count
4
+ // of `unit`. Both travel as decimal strings. All arithmetic is in BigInt nanoseconds, so a
5
+ // nanosecond timestamp round-trips exactly, and a conversion that would drop precision throws
6
+ // (E_INEXACT) instead of rounding.
7
+ //
8
+ // Calendar arithmetic follows the proleptic Gregorian calendar with no leap seconds
9
+ // (Howard Hinnant's days_from_civil / civil_from_days), not JavaScript's Date, which has only
10
+ // millisecond precision.
11
+
12
+ import { CODES, fail } from "./errors.mjs";
13
+ import { checkRange, intType, requireString, toBigInt } from "./util.mjs";
14
+
15
+ export const TIME_TYPES = ["nat", "int", "nat32", "nat64", "int64"];
16
+
17
+ const NS_PER = Object.freeze({ ns: 1n, us: 1_000n, ms: 1_000_000n, s: 1_000_000_000n });
18
+ const NS_PER_S = 1_000_000_000n;
19
+ const S_PER_DAY = 86_400n;
20
+
21
+ export function unitFactor(unit) {
22
+ const f = typeof unit === "string" && Object.hasOwn(NS_PER, unit) ? NS_PER[unit] : undefined;
23
+ if (f === undefined) fail(CODES.ARG, `unit must be one of ns, us, ms, s, got ${JSON.stringify(unit)}`);
24
+ return f;
25
+ }
26
+
27
+ /**
28
+ * Convert an integer between units. Going to a finer unit multiplies; going to a coarser unit
29
+ * requires the value to be an exact multiple, otherwise it throws E_INEXACT. Never rounds.
30
+ *
31
+ * @param {string | bigint} value
32
+ * @param {"ns"|"us"|"ms"|"s"} from
33
+ * @param {"ns"|"us"|"ms"|"s"} to
34
+ * @returns {string} decimal string
35
+ */
36
+ export function convertUnit(value, from, to) {
37
+ return nsToUnit(toBigInt(value, "value") * unitFactor(from), to).toString();
38
+ }
39
+
40
+ /**
41
+ * Floor a clock reading (ns) to a unit. `parseTimestamp` applies this to `now` before it adds a
42
+ * relative duration (spec §5.3, D-024), so a caller does not need it for parsing; it is exported
43
+ * for callers that want the aligned clock reading themselves.
44
+ *
45
+ * @param {bigint} nowNs
46
+ * @param {"ns"|"us"|"ms"|"s"} unit
47
+ * @returns {bigint}
48
+ */
49
+ export function alignNow(nowNs, unit) {
50
+ if (typeof nowNs !== "bigint") fail(CODES.TYPE, "now must be a bigint in nanoseconds");
51
+ const f = unitFactor(unit);
52
+ let q = nowNs / f;
53
+ if (nowNs % f < 0n) q -= 1n; // floor, also for pre-1970 clocks
54
+ return q * f;
55
+ }
56
+
57
+ function nsToUnit(ns, unit) {
58
+ const f = unitFactor(unit);
59
+ if (ns % f !== 0n) {
60
+ fail(CODES.INEXACT, `${ns} ns is not a whole number of ${unit}; the conversion would lose precision and is not rounded`);
61
+ }
62
+ return ns / f;
63
+ }
64
+
65
+ const floorDiv = (a, b) => {
66
+ const q = a / b;
67
+ return a % b < 0n ? q - 1n : q;
68
+ };
69
+
70
+ // ---------------------------------------------------------------- calendar
71
+
72
+ const daysFromCivil = (y0, m, d) => {
73
+ const y = m <= 2 ? y0 - 1 : y0;
74
+ const era = Math.floor(y / 400);
75
+ const yoe = y - era * 400;
76
+ const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
77
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
78
+ return era * 146097 + doe - 719468;
79
+ };
80
+
81
+ const civilFromDays = (z0) => {
82
+ const z = z0 + 719468;
83
+ const era = Math.floor(z / 146097);
84
+ const doe = z - era * 146097;
85
+ const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);
86
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));
87
+ const mp = Math.floor((5 * doy + 2) / 153);
88
+ const d = doy - Math.floor((153 * mp + 2) / 5) + 1;
89
+ const m = mp < 10 ? mp + 3 : mp - 9;
90
+ return [yoe + era * 400 + (m <= 2 ? 1 : 0), m, d];
91
+ };
92
+
93
+ const isLeap = (y) => y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
94
+ const daysInMonth = (y, m) => (m === 2 ? (isLeap(y) ? 29 : 28) : [4, 6, 9, 11].includes(m) ? 30 : 31);
95
+
96
+ const MIN_S = BigInt(daysFromCivil(0, 1, 1)) * S_PER_DAY; // 0000-01-01T00:00:00Z
97
+ const MAX_S = (BigInt(daysFromCivil(9999, 12, 31)) + 1n) * S_PER_DAY - 1n; // 9999-12-31T23:59:59Z
98
+
99
+ const pad = (n, w) => String(n).padStart(w, "0");
100
+
101
+ // ---------------------------------------------------------------- RFC 3339
102
+
103
+ const RFC3339 = /^([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?([Zz]|[+-][0-9]{2}:[0-9]{2})$/;
104
+
105
+ /** An absolute RFC 3339 time as nanoseconds since the Unix epoch. */
106
+ function parseRfc3339Ns(text) {
107
+ const m = RFC3339.exec(text);
108
+ if (!m) {
109
+ fail(
110
+ CODES.SYNTAX,
111
+ `cannot read ${JSON.stringify(text)} as a time: use RFC 3339 with an explicit zone, for example "2026-10-09T12:00:00Z" or "2026-10-09T14:00:00+02:00", or a relative time such as "+PT24H"`,
112
+ );
113
+ }
114
+ const [y, mo, d, h, mi, s] = m.slice(1, 7).map(Number);
115
+ const frac = m[7] ?? "";
116
+ if (mo < 1 || mo > 12) fail(CODES.RANGE, `month ${pad(mo, 2)} is not between 01 and 12`);
117
+ if (d < 1 || d > daysInMonth(y, mo)) fail(CODES.RANGE, `${pad(y, 4)}-${pad(mo, 2)} has no day ${pad(d, 2)}`);
118
+ if (h > 23) fail(CODES.RANGE, `hour ${pad(h, 2)} is not between 00 and 23`);
119
+ if (mi > 59) fail(CODES.RANGE, `minute ${pad(mi, 2)} is not between 00 and 59`);
120
+ if (s > 59) fail(CODES.RANGE, `second ${pad(s, 2)} is not between 00 and 59 (leap seconds have no Unix time and are rejected)`);
121
+ let offset = 0;
122
+ if (!/^[Zz]$/.test(m[8])) {
123
+ const oh = Number(m[8].slice(1, 3));
124
+ const om = Number(m[8].slice(4, 6));
125
+ if (oh > 23 || om > 59) fail(CODES.RANGE, `zone offset ${m[8]} is out of range`);
126
+ offset = (m[8][0] === "-" ? -1 : 1) * (oh * 3600 + om * 60);
127
+ }
128
+ const fracNs = BigInt(frac.slice(0, 9).padEnd(9, "0"));
129
+ if (/[1-9]/.test(frac.slice(9))) fail(CODES.INEXACT, `${JSON.stringify(text)} is finer than a nanosecond and is not rounded`);
130
+ const secs = (BigInt(daysFromCivil(y, mo, d)) * 86_400n) + BigInt(h * 3600 + mi * 60 + s - offset);
131
+ return secs * NS_PER_S + fracNs;
132
+ }
133
+
134
+ /** Canonical RFC 3339 UTC: `YYYY-MM-DDTHH:MM:SS[.f]Z`, the fraction with trailing zeros removed. */
135
+ function rfc3339FromNs(ns) {
136
+ const secs = floorDiv(ns, NS_PER_S);
137
+ const fracNs = ns - secs * NS_PER_S;
138
+ if (secs < MIN_S || secs > MAX_S) fail(CODES.RANGE, "the time is outside 0000-01-01T00:00:00Z to 9999-12-31T23:59:59Z");
139
+ const days = floorDiv(secs, S_PER_DAY);
140
+ const sod = Number(secs - days * S_PER_DAY);
141
+ const [y, mo, d] = civilFromDays(Number(days));
142
+ const frac = pad(fracNs, 9).replace(/0+$/, "");
143
+ return `${pad(y, 4)}-${pad(mo, 2)}-${pad(d, 2)}T${pad(Math.floor(sod / 3600), 2)}:${pad(Math.floor((sod % 3600) / 60), 2)}:${pad(sod % 60, 2)}${frac ? `.${frac}` : ""}Z`;
144
+ }
145
+
146
+ // ---------------------------------------------------------------- ISO 8601 durations
147
+
148
+ const DURATION =
149
+ /^(-)?P(?:([0-9]+)W)?(?:([0-9]+)D)?(?:T(?=[0-9])(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+)(?:[.,]([0-9]+))?S)?)?$/;
150
+
151
+ /** Nanoseconds in an ISO 8601 duration; only W, D, H, M (after T) and S. Returns { negative, ns }. */
152
+ function parseDurationNs(text, what) {
153
+ if (/^-?P[^T]*[YM]/.test(text)) {
154
+ fail(
155
+ CODES.CALENDAR_UNIT,
156
+ `${JSON.stringify(text)} uses years or months, which have no fixed length; use W, D, H, M or S, for example "P30D" or "PT24H"`,
157
+ );
158
+ }
159
+ const m = DURATION.exec(text);
160
+ if (!m || m.slice(2, 7).every((g) => g === undefined)) {
161
+ fail(
162
+ CODES.SYNTAX,
163
+ `cannot read ${JSON.stringify(text)} as ${what}: use ISO 8601 with W, D, H, M and S only, for example "PT24H", "P7D" or "PT1M30S"`,
164
+ );
165
+ }
166
+ const n = (g) => (g === undefined ? 0n : BigInt(g));
167
+ const frac = m[7] ?? "";
168
+ if (/[1-9]/.test(frac.slice(9))) fail(CODES.INEXACT, `${JSON.stringify(text)} is finer than a nanosecond and is not rounded`);
169
+ const ns =
170
+ ((n(m[2]) * 7n + n(m[3])) * S_PER_DAY + n(m[4]) * 3600n + n(m[5]) * 60n + n(m[6])) * NS_PER_S + BigInt(frac.slice(0, 9).padEnd(9, "0"));
171
+ return { negative: m[1] === "-", ns };
172
+ }
173
+
174
+ // ---------------------------------------------------------------- public: timestamp
175
+
176
+ /**
177
+ * Parse a timestamp: an absolute RFC 3339 time, or a relative ISO 8601 duration prefixed with "+"
178
+ * resolved against `now` floored to `unit` (spec §5.3, D-024). The floor applies to the clock
179
+ * reading only: it is a rule about the reference time, not a rounding of the user's value, so
180
+ * "+PT24H" on a `unit: s` field is exact. The duration itself and an absolute time must still
181
+ * convert exactly (E_INEXACT). Returns the integer count of `unit` as a decimal string.
182
+ *
183
+ * @param {string} input
184
+ * @param {{ unit: "ns"|"us"|"ms"|"s", candid?: string, now?: bigint }} opts `now` is nanoseconds
185
+ * since the Unix epoch from the canister's clock (spec §4), required only for relative input.
186
+ */
187
+ export function parseTimestamp(input, { unit, candid = "nat64", now } = {}) {
188
+ unitFactor(unit);
189
+ const type = intType(candid, TIME_TYPES, "timestamp");
190
+ const text = requireString(input, "timestamp").trim();
191
+ let ns;
192
+ if (text.startsWith("+")) {
193
+ if (typeof now !== "bigint") fail(CODES.NEEDS_NOW, `"${text}" is relative to the current time, but no \`now\` (a bigint in nanoseconds) was supplied`);
194
+ const { negative, ns: dur } = parseDurationNs(text.slice(1), "a relative time");
195
+ if (negative) fail(CODES.SYNTAX, `a relative time is "+" followed by a duration, not ${JSON.stringify(text)}`);
196
+ ns = alignNow(now, unit) + dur;
197
+ } else {
198
+ ns = parseRfc3339Ns(text);
199
+ }
200
+ const v = nsToUnit(ns, unit);
201
+ checkRange(v, type, "timestamp");
202
+ return v.toString();
203
+ }
204
+
205
+ /**
206
+ * Display a timestamp as canonical RFC 3339 UTC, for example "2026-10-09T12:00:00.5Z".
207
+ *
208
+ * @param {string | bigint} value integer count of `unit` since the Unix epoch
209
+ */
210
+ export function displayTimestamp(value, { unit, candid = "nat64" } = {}) {
211
+ const f = unitFactor(unit);
212
+ const type = intType(candid, TIME_TYPES, "timestamp");
213
+ const v = toBigInt(value, "timestamp");
214
+ checkRange(v, type, "timestamp");
215
+ return rfc3339FromNs(v * f);
216
+ }
217
+
218
+ // ---------------------------------------------------------------- public: duration
219
+
220
+ /**
221
+ * Parse an ISO 8601 duration made only of W, D, H, M (after T) and S, for example "PT24H".
222
+ * A fraction is allowed on the seconds only ("PT0.5S"). A leading "-" is allowed for signed types.
223
+ * Returns the integer count of `unit` as a decimal string.
224
+ */
225
+ export function parseDuration(input, { unit, candid = "nat64" } = {}) {
226
+ unitFactor(unit);
227
+ const type = intType(candid, TIME_TYPES, "duration");
228
+ const text = requireString(input, "duration").trim();
229
+ const { negative, ns } = parseDurationNs(text, "a duration");
230
+ if (negative && ns !== 0n && !type.signed) fail(CODES.NEGATIVE, `negative durations are not allowed here (the Candid type ${candid} is unsigned)`);
231
+ const v = nsToUnit(negative ? -ns : ns, unit);
232
+ checkRange(v, type, "duration");
233
+ return v.toString();
234
+ }
235
+
236
+ /**
237
+ * Display a duration as canonical ISO 8601: days, hours, minutes and seconds, each only when
238
+ * non-zero; never weeks, years or months. 86,400 s is "P1D", 5,400 s is "PT1H30M", zero is "PT0S".
239
+ * The seconds carry a fraction when the unit is finer than a second ("PT0.5S").
240
+ */
241
+ export function displayDuration(value, { unit, candid = "nat64" } = {}) {
242
+ const f = unitFactor(unit);
243
+ const type = intType(candid, TIME_TYPES, "duration");
244
+ const v = toBigInt(value, "duration");
245
+ checkRange(v, type, "duration");
246
+ const total = (v < 0n ? -v : v) * f;
247
+ const secs = total / NS_PER_S;
248
+ const frac = pad(total % NS_PER_S, 9).replace(/0+$/, "");
249
+ const days = secs / S_PER_DAY;
250
+ const h = (secs % S_PER_DAY) / 3600n;
251
+ const mi = (secs % 3600n) / 60n;
252
+ const s = secs % 60n;
253
+ let out = days ? `${days}D` : "";
254
+ let t = `${h ? `${h}H` : ""}${mi ? `${mi}M` : ""}${s || frac ? `${s}${frac ? `.${frac}` : ""}S` : ""}`;
255
+ if (!out && !t) t = "0S";
256
+ if (t) out += `T${t}`;
257
+ return `${v < 0n ? "-" : ""}P${out}`;
258
+ }
package/src/util.mjs ADDED
@@ -0,0 +1,139 @@
1
+ import { CODES, fail } from "./errors.mjs";
2
+
3
+ /** Bound on the length of any text this package parses (bounded work, spec §11.6). */
4
+ export const MAX_INPUT_CHARS = 1024;
5
+
6
+ const DEC_INT = /^-?(?:0|[1-9][0-9]*)$/;
7
+
8
+ /** The Candid integer types the value tags apply to, with their inclusive bounds (null = unbounded). */
9
+ export const INT_TYPES = Object.freeze({
10
+ nat: { signed: false, min: 0n, max: null },
11
+ nat8: { signed: false, min: 0n, max: 2n ** 8n - 1n },
12
+ nat16: { signed: false, min: 0n, max: 2n ** 16n - 1n },
13
+ nat32: { signed: false, min: 0n, max: 2n ** 32n - 1n },
14
+ nat64: { signed: false, min: 0n, max: 2n ** 64n - 1n },
15
+ int: { signed: true, min: null, max: null },
16
+ int8: { signed: true, min: -(2n ** 7n), max: 2n ** 7n - 1n },
17
+ int16: { signed: true, min: -(2n ** 15n), max: 2n ** 15n - 1n },
18
+ int32: { signed: true, min: -(2n ** 31n), max: 2n ** 31n - 1n },
19
+ int64: { signed: true, min: -(2n ** 63n), max: 2n ** 63n - 1n },
20
+ });
21
+
22
+ /** @param {string} candid @param {string[]} allowed */
23
+ export function intType(candid, allowed, tag) {
24
+ if (!allowed.includes(candid)) {
25
+ fail(CODES.TAG_TYPE, `the ${tag} tag applies to ${allowed.join(", ")}, not ${candid}`);
26
+ }
27
+ return INT_TYPES[candid];
28
+ }
29
+
30
+ export function checkLength(text, what) {
31
+ if (text.length > MAX_INPUT_CHARS) fail(CODES.TOO_LONG, `${what} is longer than ${MAX_INPUT_CHARS} characters`);
32
+ }
33
+
34
+ export function requireString(input, what) {
35
+ if (typeof input !== "string") fail(CODES.TYPE, `${what} must be a string, got ${typeof input}`);
36
+ checkLength(input, what);
37
+ return input;
38
+ }
39
+
40
+ /**
41
+ * A base-unit integer as a bigint. Accepts a canonical decimal string or a bigint. A JavaScript
42
+ * number is rejected whatever its value: big integers must never pass through a double (D-006).
43
+ */
44
+ export function toBigInt(value, what = "value") {
45
+ if (typeof value === "bigint") return value;
46
+ if (typeof value === "string") {
47
+ if (value.length > MAX_INPUT_CHARS) fail(CODES.TOO_LONG, `${what} is too long`);
48
+ if (!DEC_INT.test(value)) fail(CODES.SYNTAX, `${what} must be a canonical decimal integer string, got ${JSON.stringify(value)}`);
49
+ if (value === "-0") fail(CODES.SYNTAX, `${what} must not be "-0"`);
50
+ return BigInt(value);
51
+ }
52
+ fail(CODES.TYPE, `${what} must be a decimal string or a bigint, not ${typeof value} (integers are never passed as JavaScript numbers)`);
53
+ }
54
+
55
+ export function checkRange(v, type, what) {
56
+ if (!type.signed && v < 0n) fail(CODES.RANGE, `${what} ${v} is negative but the Candid type is unsigned`);
57
+ if (type.min !== null && v < type.min) fail(CODES.RANGE, `${what} ${v} is below the minimum ${type.min} of the Candid type`);
58
+ if (type.max !== null && v > type.max) fail(CODES.RANGE, `${what} ${v} is above the maximum ${type.max} of the Candid type`);
59
+ }
60
+
61
+ // ---------------------------------------------------------------- bytes helpers
62
+
63
+ const HEX = "0123456789abcdef";
64
+
65
+ export function bytesToHex(bytes) {
66
+ let out = "";
67
+ for (const b of bytes) out += HEX[b >>> 4] + HEX[b & 15];
68
+ return out;
69
+ }
70
+
71
+ /** Strict hex: even length, hex digits only, either case. Returns null when invalid. */
72
+ export function hexToBytes(hex) {
73
+ if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) return null;
74
+ const out = new Uint8Array(hex.length / 2);
75
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
76
+ return out;
77
+ }
78
+
79
+ const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
80
+ const intrinsic = (key) => Object.getOwnPropertyDescriptor(TypedArrayPrototype, key).get;
81
+ const typedArrayTag = intrinsic(Symbol.toStringTag);
82
+ const viewBuffer = intrinsic("buffer");
83
+ const viewOffset = intrinsic("byteOffset");
84
+ const viewLength = intrinsic("byteLength");
85
+ const bufferDetached = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "detached")?.get;
86
+ /**
87
+ * Whether the buffer is a detached ArrayBuffer (a SharedArrayBuffer never is). Where `ArrayBuffer.prototype.detached`
88
+ * is missing (older browsers), a view of a detached buffer cannot be constructed, so trying one is the test.
89
+ */
90
+ const isDetached = (buffer) => {
91
+ if (bufferDetached !== undefined) {
92
+ try {
93
+ return bufferDetached.call(buffer) === true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ try {
99
+ new Uint8Array(buffer, 0, 0);
100
+ return false;
101
+ } catch {
102
+ return true;
103
+ }
104
+ };
105
+
106
+ /**
107
+ * The value as a Uint8Array of this realm, or null. Accepts a Uint8Array made in another realm (a `node:vm`
108
+ * context, an iframe, a test environment's TextEncoder) by reading its typed-array kind from the internal slot,
109
+ * which a plain object (or `Object.create(Uint8Array.prototype)`) cannot fake, and viewing the same memory (no
110
+ * copy). The memory is read through the same intrinsic getters, never `value.buffer` and friends, which an own
111
+ * property could override to show other bytes than the view holds. Other typed arrays, DataViews, Proxies and
112
+ * views of a detached buffer are not bytes.
113
+ */
114
+ export function asUint8Array(value) {
115
+ if (!ArrayBuffer.isView(value) || typedArrayTag.call(value) !== "Uint8Array") return null;
116
+ const buffer = viewBuffer.call(value);
117
+ if (isDetached(buffer)) return null;
118
+ if (value instanceof Uint8Array && Object.getPrototypeOf(value) === Uint8Array.prototype) return value;
119
+ try {
120
+ return new Uint8Array(buffer, viewOffset.call(value), viewLength.call(value));
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /** Uint8Array (from any realm), or an array of integers 0..255 (the JSON form of a blob). */
127
+ export function toBytes(value, what = "bytes") {
128
+ const view = asUint8Array(value);
129
+ if (view !== null) return view;
130
+ if (Array.isArray(value) && value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) return Uint8Array.from(value);
131
+ return fail(CODES.TYPE, `${what} must be a Uint8Array or an array of byte values`);
132
+ }
133
+
134
+ /**
135
+ * Characters that must never be shown inline in a consent or tool text: control characters (Cc),
136
+ * format characters such as bidirectional overrides and zero-width joiners (Cf), and line and
137
+ * paragraph separators (Zl, Zp).
138
+ */
139
+ export const UNSAFE_DISPLAY_CHAR = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;