@fuzdev/fuz_app 0.142.0 → 0.144.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/dist/auth/CLAUDE.md +8 -0
- package/dist/auth/cell_action_specs.d.ts +13 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +13 -0
- package/dist/auth/cell_actions.d.ts +7 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +30 -0
- package/dist/auth/cell_ref_admission.d.ts +51 -0
- package/dist/auth/cell_ref_admission.d.ts.map +1 -0
- package/dist/auth/cell_ref_admission.js +69 -0
- package/dist/auth/deps.d.ts +6 -3
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/db/CLAUDE.md +34 -21
- package/dist/db/cell_ddl.d.ts +3 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +3 -2
- package/dist/db/cell_queries.d.ts +31 -6
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +45 -18
- package/dist/db/fact_ddl.d.ts +29 -4
- package/dist/db/fact_ddl.d.ts.map +1 -1
- package/dist/db/fact_ddl.js +43 -4
- package/dist/db/fact_holder_queries.d.ts +66 -0
- package/dist/db/fact_holder_queries.d.ts.map +1 -0
- package/dist/db/fact_holder_queries.js +75 -0
- package/dist/db/fact_queries.d.ts +9 -7
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_store.d.ts +65 -9
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +49 -20
- package/dist/http/CLAUDE.md +1 -1
- package/dist/http/ip_canonical.d.ts +26 -2
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +107 -42
- package/dist/server/serve_fact_route.d.ts +24 -4
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +24 -4
- package/dist/testing/CLAUDE.md +31 -5
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +6 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +25 -16
- package/dist/testing/cross_backend/cell_ref_admission.d.ts +4 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_ref_admission.js +268 -0
- package/dist/testing/cross_backend/expected_schema.json +66 -10
- package/dist/testing/cross_backend/fact_serving.d.ts +19 -1
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +31 -9
- package/dist/testing/cross_backend/full_spine_mount.js +2 -1
- package/dist/testing/cross_backend/origin_conformance.d.ts +1 -1
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin_conformance.js +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +12 -20
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +12 -20
- package/dist/testing/cross_backend/testing_origin_check.d.ts +128 -0
- package/dist/testing/cross_backend/testing_origin_check.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_origin_check.js +87 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +13 -121
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +21 -95
- package/package.json +1 -1
package/dist/db/fact_store.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PG-backed `
|
|
2
|
+
* PG-backed `HeldFactStore` implementation.
|
|
3
3
|
*
|
|
4
4
|
* Wraps the raw queries in `db/fact_queries.ts` with the lifecycle the
|
|
5
|
-
* `
|
|
5
|
+
* `HeldFactStore` interface promises:
|
|
6
6
|
*
|
|
7
7
|
* - sync hash on `put`; `put_stream` hashes as it streams
|
|
8
8
|
* - idempotent insert (`ON CONFLICT DO NOTHING` in the queries layer)
|
|
@@ -33,18 +33,32 @@
|
|
|
33
33
|
* `external_url` has any other shape, and any external row on a store with no
|
|
34
34
|
* disk CAS configured, as unavailable: a warning and `null`.
|
|
35
35
|
*
|
|
36
|
+
* ## Holders
|
|
37
|
+
*
|
|
38
|
+
* Every put names its holder (`HeldFactPutOptions.holder`): the actor whose
|
|
39
|
+
* request carried the bytes, or `null` for a system put (seeds, server-side
|
|
40
|
+
* imports). A named holder gets a `fact_holder` row on every put — a
|
|
41
|
+
* deduplicated put included, since bringing identical bytes is possession. The
|
|
42
|
+
* `fact` row, its refs, and the holder row commit in one transaction. Cell ref
|
|
43
|
+
* admission (`auth/cell_ref_admission.ts`) reads the holder rows. The holder is
|
|
44
|
+
* a spine concept, so `PgFactStore` implements `HeldFactStore` — the
|
|
45
|
+
* `@fuzdev/fuz_util` `FactStore` reads plus puts that take `HeldFactPutOptions` —
|
|
46
|
+
* rather than the backend-agnostic `FactStore`, whose puts name no actor.
|
|
47
|
+
*
|
|
36
48
|
* @module
|
|
37
49
|
*/
|
|
38
50
|
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
39
51
|
import { fact_hash_bytes, fact_hash_verify, fact_hash_extract_refs } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
40
52
|
import { query_delete_fact, query_get_fact, query_get_fact_meta, query_get_fact_refs, query_has_fact, query_put_fact, query_put_fact_refs } from './fact_queries.js';
|
|
53
|
+
import { query_put_fact_holder } from './fact_holder_queries.js';
|
|
41
54
|
import { is_file_fact_url } from './file_fact_url.js';
|
|
42
55
|
import { read_fact_bytes_from_disk, stream_fact_to_disk, write_fact_bytes_to_disk } from './fact_disk_storage.js';
|
|
43
56
|
/** Default embedded-vs-referenced cutoff (1 MiB). */
|
|
44
57
|
export const FACT_EMBEDDED_THRESHOLD_DEFAULT = 1024 * 1024;
|
|
45
58
|
/**
|
|
46
|
-
* PG-backed
|
|
47
|
-
* the lifecycle layer described in the module doc.
|
|
59
|
+
* PG-backed fact store. Delegates to `db/fact_queries.ts` for I/O and adds
|
|
60
|
+
* the lifecycle layer described in the module doc. Implements `HeldFactStore`:
|
|
61
|
+
* the `@fuzdev/fuz_util` `FactStore` reads, puts taking `HeldFactPutOptions`.
|
|
48
62
|
*/
|
|
49
63
|
export class PgFactStore {
|
|
50
64
|
#deps;
|
|
@@ -65,7 +79,10 @@ export class PgFactStore {
|
|
|
65
79
|
* are configured) at `<facts_dir>/<shard>/<rest>` with a `file:` URL. Oversize
|
|
66
80
|
* without a disk root throws: the store has nowhere to put bytes it cannot
|
|
67
81
|
* embed. Idempotent — `ON CONFLICT DO NOTHING` + content-addressed disk
|
|
68
|
-
* filenames make a re-write a no-op.
|
|
82
|
+
* filenames make a re-write a no-op. `options.holder`, when set, is recorded
|
|
83
|
+
* as a holder on a fresh insert and a dedup hit alike.
|
|
84
|
+
*
|
|
85
|
+
* @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
|
|
69
86
|
*/
|
|
70
87
|
async put(bytes, options) {
|
|
71
88
|
const hash = fact_hash_bytes(bytes);
|
|
@@ -82,16 +99,13 @@ export class PgFactStore {
|
|
|
82
99
|
row_bytes = bytes;
|
|
83
100
|
row_external_url = null;
|
|
84
101
|
}
|
|
85
|
-
|
|
102
|
+
await this.#insert_row({
|
|
86
103
|
hash,
|
|
87
104
|
bytes: row_bytes,
|
|
88
105
|
external_url: row_external_url,
|
|
89
|
-
content_type: options
|
|
106
|
+
content_type: options.content_type ?? null,
|
|
90
107
|
size: bytes.length
|
|
91
|
-
});
|
|
92
|
-
if (inserted) {
|
|
93
|
-
await query_put_fact_refs(this.#deps, hash, resolve_refs(bytes, options));
|
|
94
|
-
}
|
|
108
|
+
}, resolve_refs(bytes, options), options.holder);
|
|
95
109
|
return hash;
|
|
96
110
|
}
|
|
97
111
|
/**
|
|
@@ -109,7 +123,9 @@ export class PgFactStore {
|
|
|
109
123
|
*
|
|
110
124
|
* Requires `fs` (and, for the over-threshold spill, `disk_root`) to be
|
|
111
125
|
* configured. The streaming twin of `put`; mirrors the Rust
|
|
112
|
-
* `FactStore::put_stream`.
|
|
126
|
+
* `FactStore::put_stream`. Records `options.holder` the way `put` does.
|
|
127
|
+
*
|
|
128
|
+
* @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
|
|
113
129
|
*/
|
|
114
130
|
async put_stream(stream, max_bytes, options) {
|
|
115
131
|
if (this.#fs === undefined) {
|
|
@@ -118,18 +134,31 @@ export class PgFactStore {
|
|
|
118
134
|
const streamed = await stream_fact_to_disk(this.#fs, this.#disk_root, stream, max_bytes, this.#embedded_threshold);
|
|
119
135
|
const row_bytes = streamed.placement.kind === 'embedded' ? streamed.placement.bytes : null;
|
|
120
136
|
const row_external_url = streamed.placement.kind === 'disk' ? streamed.placement.external_url : null;
|
|
121
|
-
|
|
137
|
+
await this.#insert_row({
|
|
122
138
|
hash: streamed.hash,
|
|
123
139
|
bytes: row_bytes,
|
|
124
140
|
external_url: row_external_url,
|
|
125
|
-
content_type: options
|
|
141
|
+
content_type: options.content_type ?? null,
|
|
126
142
|
size: streamed.size
|
|
127
|
-
});
|
|
128
|
-
if (inserted && options?.refs && options.refs.length > 0) {
|
|
129
|
-
await query_put_fact_refs(this.#deps, streamed.hash, options.refs);
|
|
130
|
-
}
|
|
143
|
+
}, options.refs ?? [], options.holder);
|
|
131
144
|
return { hash: streamed.hash, sha256: streamed.sha256, size: streamed.size };
|
|
132
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Insert the `fact` row, then — in the same transaction — its refs (fresh
|
|
148
|
+
* insert only) and the holder row (every put, a dedup hit included). Shared
|
|
149
|
+
* by `put` and `put_stream`; twin of the Rust `PgFactStore::insert_row`.
|
|
150
|
+
*/
|
|
151
|
+
async #insert_row(row, refs, holder) {
|
|
152
|
+
await this.#deps.db.transaction(async (db) => {
|
|
153
|
+
const inserted = await query_put_fact({ db }, row);
|
|
154
|
+
if (inserted && refs.length > 0) {
|
|
155
|
+
await query_put_fact_refs({ db }, row.hash, refs);
|
|
156
|
+
}
|
|
157
|
+
if (holder !== null) {
|
|
158
|
+
await query_put_fact_holder({ db }, holder, row.hash);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
133
162
|
/**
|
|
134
163
|
* Retrieve bytes. Embedded reads return PG bytes directly. An external
|
|
135
164
|
* read comes from the disk CAS and nowhere else: the bytes at the row's
|
|
@@ -219,9 +248,9 @@ export class PgFactStore {
|
|
|
219
248
|
* from JSON content; otherwise no refs.
|
|
220
249
|
*/
|
|
221
250
|
const resolve_refs = (bytes, options) => {
|
|
222
|
-
if (options
|
|
251
|
+
if (options.refs !== undefined)
|
|
223
252
|
return options.refs;
|
|
224
|
-
if (options
|
|
253
|
+
if (options.content_type !== 'application/json')
|
|
225
254
|
return [];
|
|
226
255
|
let value;
|
|
227
256
|
try {
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -24,7 +24,7 @@ effects, see ../../../docs/architecture.md.
|
|
|
24
24
|
- `http/surface_query.ts` — pure filters/groupings over `AppSurface`.
|
|
25
25
|
- `http/proxy.ts` — trusted-proxy middleware, CIDR parsing, rightmost-first XFF resolution. Pulls `hono/utils/ipaddr` for CIDR matching.
|
|
26
26
|
- `http/client_ip.ts` — `get_client_ip(c)` — reads the resolved IP off the context (set by `proxy.ts`'s middleware). Split out of `proxy.ts` so it's hono-free: dispatch + route modules that only _read_ the IP don't pull `hono/utils/ipaddr`, keeping cross-process test surfaces free of the optional `hono` peer.
|
|
27
|
-
- `http/ip_canonical.ts` — RFC 5952 IPv6 canonicalization + IPv4-mapped collapse; `IP_LITERAL_CHARS` regex.
|
|
27
|
+
- `http/ip_canonical.ts` — RFC 5952 IPv6 canonicalization + IPv4-mapped collapse, `rate_limit_ip_key` (an IPv6 client's `/64`), and its own `parse_ipv4_bits` / `parse_ipv6_bits` (the grammar of Rust's `IpAddr::from_str`); `IP_LITERAL_CHARS` regex. Imports nothing — the dispatcher keys its IP limiter through it, so it must stay hono-free (`src/test/hono_free_modules.test.ts` walks every `src/lib` module's graph).
|
|
28
28
|
- `http/origin.ts` — origin allowlist: the strict pattern grammar shared with the Rust spine, typed refusals, and the Origin-only middleware.
|
|
29
29
|
- `http/jsonrpc.ts` — JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta`.
|
|
30
30
|
- `http/jsonrpc_errors.ts` — `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings.
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* strings (`'unknown'`, `'attacker:controlled'`, `'::1\n'`) pass
|
|
19
19
|
* through unchanged so downstream strict validators can still
|
|
20
20
|
* reject them.
|
|
21
|
-
* 2. Parses
|
|
21
|
+
* 2. Parses with `parse_ipv4_bits` / `parse_ipv6_bits` — the grammar of
|
|
22
|
+
* Rust's `IpAddr::from_str`, which the spine parses with.
|
|
22
23
|
* 3. Re-emits the canonical RFC 5952 string (lowercase hex,
|
|
23
24
|
* longest-zero-run compressed, IPv4-mapped emitted in the dotted
|
|
24
25
|
* form mandated by RFC 5952 §5).
|
|
@@ -37,8 +38,31 @@
|
|
|
37
38
|
* the limiter call sites apply it — the client IP stays the full address
|
|
38
39
|
* everywhere else (audit rows, token `last_used_ip`, `REMOTE_ADDR`).
|
|
39
40
|
*
|
|
41
|
+
* The module imports nothing: the action dispatcher keys its IP limiter
|
|
42
|
+
* through it, so it must not pull the optional `hono` peer.
|
|
43
|
+
*
|
|
40
44
|
* @module
|
|
41
45
|
*/
|
|
46
|
+
/**
|
|
47
|
+
* Parse a dotted-quad IPv4 address to its 32-bit value.
|
|
48
|
+
*
|
|
49
|
+
* Four decimal octets, each 0–255 with no leading zeros (`01.2.3.4` is
|
|
50
|
+
* refused, as Rust's `Ipv4Addr::from_str` refuses it).
|
|
51
|
+
*
|
|
52
|
+
* @returns the address as an unsigned 32-bit number, or `null` when `ip` isn't one
|
|
53
|
+
*/
|
|
54
|
+
export declare const parse_ipv4_bits: (ip: string) => number | null;
|
|
55
|
+
/**
|
|
56
|
+
* Parse an IPv6 address to its 128-bit value.
|
|
57
|
+
*
|
|
58
|
+
* The grammar of Rust's `Ipv6Addr::from_str`: eight groups of one to four
|
|
59
|
+
* hex digits (either case), or fewer around a single `::` standing for one or
|
|
60
|
+
* more zero groups, the last two groups optionally written as a dotted-quad
|
|
61
|
+
* IPv4 address. No zone (`%eth0`) — callers strip it first.
|
|
62
|
+
*
|
|
63
|
+
* @returns the address as a `bigint` in `[0, 2^128)`, or `null` when `ip` isn't one
|
|
64
|
+
*/
|
|
65
|
+
export declare const parse_ipv6_bits: (ip: string) => bigint | null;
|
|
42
66
|
/**
|
|
43
67
|
* Allowed character set for a bare IP literal.
|
|
44
68
|
*
|
|
@@ -95,7 +119,7 @@ export declare const canonicalize_ip: (ip: string) => string;
|
|
|
95
119
|
*
|
|
96
120
|
* Pure helper exported for the test suite to exercise the
|
|
97
121
|
* canonicalization invariants directly without a full
|
|
98
|
-
* `
|
|
122
|
+
* `parse_ipv6_bits` round-trip.
|
|
99
123
|
*
|
|
100
124
|
* @param bits - the 128-bit IPv6 value as `bigint`. Must satisfy `0n <= bits < 2n ** 128n`;
|
|
101
125
|
* throws `RangeError` otherwise. Silent truncation would mask caller bugs since the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ip_canonical.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/ip_canonical.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ip_canonical.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/ip_canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AASH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAAM,GAAG,IAKrD,CAAC;AA0BF;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAAM,GAAG,IAkBrD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,QAAqB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAsB5C,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,MAgEvD,CAAC;AAKF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,MAAM,KAAG,MAQ9C,CAAC"}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* strings (`'unknown'`, `'attacker:controlled'`, `'::1\n'`) pass
|
|
19
19
|
* through unchanged so downstream strict validators can still
|
|
20
20
|
* reject them.
|
|
21
|
-
* 2. Parses
|
|
21
|
+
* 2. Parses with `parse_ipv4_bits` / `parse_ipv6_bits` — the grammar of
|
|
22
|
+
* Rust's `IpAddr::from_str`, which the spine parses with.
|
|
22
23
|
* 3. Re-emits the canonical RFC 5952 string (lowercase hex,
|
|
23
24
|
* longest-zero-run compressed, IPv4-mapped emitted in the dotted
|
|
24
25
|
* form mandated by RFC 5952 §5).
|
|
@@ -37,9 +38,92 @@
|
|
|
37
38
|
* the limiter call sites apply it — the client IP stays the full address
|
|
38
39
|
* everywhere else (audit rows, token `last_used_ip`, `REMOTE_ADDR`).
|
|
39
40
|
*
|
|
41
|
+
* The module imports nothing: the action dispatcher keys its IP limiter
|
|
42
|
+
* through it, so it must not pull the optional `hono` peer.
|
|
43
|
+
*
|
|
40
44
|
* @module
|
|
41
45
|
*/
|
|
42
|
-
|
|
46
|
+
/** A dotted-quad IPv4 address: four decimal octets, 0–255, no leading zeros. */
|
|
47
|
+
const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/;
|
|
48
|
+
/** One IPv6 group: one to four hex digits. */
|
|
49
|
+
const IPV6_GROUP_REGEX = /^[0-9a-fA-F]{1,4}$/;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a dotted-quad IPv4 address to its 32-bit value.
|
|
52
|
+
*
|
|
53
|
+
* Four decimal octets, each 0–255 with no leading zeros (`01.2.3.4` is
|
|
54
|
+
* refused, as Rust's `Ipv4Addr::from_str` refuses it).
|
|
55
|
+
*
|
|
56
|
+
* @returns the address as an unsigned 32-bit number, or `null` when `ip` isn't one
|
|
57
|
+
*/
|
|
58
|
+
export const parse_ipv4_bits = (ip) => {
|
|
59
|
+
if (!IPV4_REGEX.test(ip))
|
|
60
|
+
return null;
|
|
61
|
+
let bits = 0;
|
|
62
|
+
for (const octet of ip.split('.'))
|
|
63
|
+
bits = bits * 256 + Number(octet);
|
|
64
|
+
return bits;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Parse the colon-separated groups on one side of a `::` (or the whole
|
|
68
|
+
* address when there is none), `null` on a malformed one. Only the last group
|
|
69
|
+
* of the address may be a dotted-quad IPv4 tail, which fills two groups.
|
|
70
|
+
*/
|
|
71
|
+
const parse_ipv6_groups = (part, allow_ipv4_tail) => {
|
|
72
|
+
if (part === '')
|
|
73
|
+
return [];
|
|
74
|
+
const pieces = part.split(':');
|
|
75
|
+
const groups = [];
|
|
76
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
77
|
+
const piece = pieces[i];
|
|
78
|
+
if (allow_ipv4_tail && i === pieces.length - 1 && piece.includes('.')) {
|
|
79
|
+
const ipv4 = parse_ipv4_bits(piece);
|
|
80
|
+
if (ipv4 === null)
|
|
81
|
+
return null;
|
|
82
|
+
groups.push(Math.floor(ipv4 / 0x10000), ipv4 % 0x10000);
|
|
83
|
+
}
|
|
84
|
+
else if (IPV6_GROUP_REGEX.test(piece)) {
|
|
85
|
+
groups.push(parseInt(piece, 16));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return groups;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Parse an IPv6 address to its 128-bit value.
|
|
95
|
+
*
|
|
96
|
+
* The grammar of Rust's `Ipv6Addr::from_str`: eight groups of one to four
|
|
97
|
+
* hex digits (either case), or fewer around a single `::` standing for one or
|
|
98
|
+
* more zero groups, the last two groups optionally written as a dotted-quad
|
|
99
|
+
* IPv4 address. No zone (`%eth0`) — callers strip it first.
|
|
100
|
+
*
|
|
101
|
+
* @returns the address as a `bigint` in `[0, 2^128)`, or `null` when `ip` isn't one
|
|
102
|
+
*/
|
|
103
|
+
export const parse_ipv6_bits = (ip) => {
|
|
104
|
+
const halves = ip.split('::');
|
|
105
|
+
let groups;
|
|
106
|
+
if (halves.length === 1) {
|
|
107
|
+
const all = parse_ipv6_groups(ip, true);
|
|
108
|
+
if (all === null || all.length !== 8)
|
|
109
|
+
return null;
|
|
110
|
+
groups = all;
|
|
111
|
+
}
|
|
112
|
+
else if (halves.length === 2) {
|
|
113
|
+
const head = parse_ipv6_groups(halves[0], false);
|
|
114
|
+
const tail = parse_ipv6_groups(halves[1], true);
|
|
115
|
+
if (head === null || tail === null || head.length + tail.length > 7)
|
|
116
|
+
return null;
|
|
117
|
+
groups = [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail];
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
let bits = 0n;
|
|
123
|
+
for (const group of groups)
|
|
124
|
+
bits = (bits << 16n) | BigInt(group);
|
|
125
|
+
return bits;
|
|
126
|
+
};
|
|
43
127
|
/**
|
|
44
128
|
* Allowed character set for a bare IP literal.
|
|
45
129
|
*
|
|
@@ -86,39 +170,27 @@ export const IP_LITERAL_CHARS = /^[0-9a-fA-F.:]+$/;
|
|
|
86
170
|
export const canonicalize_ip = (ip) => {
|
|
87
171
|
const lowered = ip.toLowerCase();
|
|
88
172
|
// Strict char-set filter — reject brackets, whitespace, control bytes,
|
|
89
|
-
// letters g-z before invoking the parser
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// longer reject it. Pass-through preserves the original string.
|
|
173
|
+
// letters g-z, and `%` (so a zone) before invoking the parser, so a
|
|
174
|
+
// malformed form passes through for downstream `validate_ip_strict` to
|
|
175
|
+
// reject
|
|
93
176
|
if (!IP_LITERAL_CHARS.test(lowered))
|
|
94
177
|
return lowered;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// needed. Malformed forms (`999.999.999.999`) still pass through
|
|
99
|
-
// here; downstream `validate_ip_strict` rejects them via its own
|
|
100
|
-
// round-trip parse.
|
|
178
|
+
// IPv4's dotted-decimal form is already canonical — no transform needed.
|
|
179
|
+
// Malformed forms (`999.999.999.999`) fail both parsers and pass through.
|
|
180
|
+
if (parse_ipv4_bits(lowered) !== null)
|
|
101
181
|
return lowered;
|
|
182
|
+
const bits = parse_ipv6_bits(lowered);
|
|
183
|
+
if (bits === null)
|
|
184
|
+
return lowered;
|
|
185
|
+
const canonical = ipv6_bigint_to_canonical(bits);
|
|
186
|
+
// Strip `::ffff:` only when the canonical form is dotted IPv4-mapped
|
|
187
|
+
// (`::ffff:X.X.X.X`). Pure IPv6 values that happen to start with
|
|
188
|
+
// `::ffff:` (e.g. `::ffff:1` → `0:0:0:0:0:0:ffff:1`, where group[5] is 0
|
|
189
|
+
// not 0xffff) emit without the dot and are preserved.
|
|
190
|
+
if (canonical.startsWith('::ffff:') && canonical.substring(7).includes('.')) {
|
|
191
|
+
return canonical.substring(7);
|
|
102
192
|
}
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const bits = convertIPv6ToBinary(lowered);
|
|
106
|
-
const canonical = ipv6_bigint_to_canonical(bits);
|
|
107
|
-
// Strip `::ffff:` only when the canonical form is dotted
|
|
108
|
-
// IPv4-mapped (`::ffff:X.X.X.X`). Pure IPv6 values that happen
|
|
109
|
-
// to start with `::ffff:` (e.g. `::ffff:1` → `0:0:0:0:0:0:ffff:1`,
|
|
110
|
-
// where group[5] is 0 not 0xffff) emit without the dot and
|
|
111
|
-
// are preserved.
|
|
112
|
-
if (canonical.startsWith('::ffff:') && canonical.substring(7).includes('.')) {
|
|
113
|
-
return canonical.substring(7);
|
|
114
|
-
}
|
|
115
|
-
return canonical;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return lowered;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return lowered;
|
|
193
|
+
return canonical;
|
|
122
194
|
};
|
|
123
195
|
/**
|
|
124
196
|
* Convert a 128-bit IPv6 binary value into its RFC 5952 canonical string form.
|
|
@@ -132,7 +204,7 @@ export const canonicalize_ip = (ip) => {
|
|
|
132
204
|
*
|
|
133
205
|
* Pure helper exported for the test suite to exercise the
|
|
134
206
|
* canonicalization invariants directly without a full
|
|
135
|
-
* `
|
|
207
|
+
* `parse_ipv6_bits` round-trip.
|
|
136
208
|
*
|
|
137
209
|
* @param bits - the 128-bit IPv6 value as `bigint`. Must satisfy `0n <= bits < 2n ** 128n`;
|
|
138
210
|
* throws `RangeError` otherwise. Silent truncation would mask caller bugs since the
|
|
@@ -226,17 +298,10 @@ export const rate_limit_ip_key = (ip) => {
|
|
|
226
298
|
const canonical = canonicalize_ip(zone_at === -1 ? ip : ip.slice(0, zone_at));
|
|
227
299
|
if (!IP_LITERAL_CHARS.test(canonical))
|
|
228
300
|
return ip;
|
|
229
|
-
|
|
230
|
-
if (family === 'IPv4')
|
|
301
|
+
if (parse_ipv4_bits(canonical) !== null)
|
|
231
302
|
return canonical;
|
|
232
|
-
|
|
303
|
+
const bits = parse_ipv6_bits(canonical);
|
|
304
|
+
if (bits === null)
|
|
233
305
|
return ip;
|
|
234
|
-
let bits;
|
|
235
|
-
try {
|
|
236
|
-
bits = convertIPv6ToBinary(canonical);
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
return ip;
|
|
240
|
-
}
|
|
241
306
|
return `${ipv6_bigint_to_canonical(bits & IPV6_PREFIX_64_MASK)}/64`;
|
|
242
307
|
};
|
|
@@ -41,6 +41,25 @@
|
|
|
41
41
|
* hash. B publishing identical bytes from B's public cell makes them
|
|
42
42
|
* readable *via B's cell* — it never touches A's private reference.
|
|
43
43
|
*
|
|
44
|
+
* The edge is only as safe as what may create it. A cell's `refs` derive
|
|
45
|
+
* from its `data`, and any account that can write a cell writes its `data` —
|
|
46
|
+
* so this route alone would make a known hash a read capability: name it in
|
|
47
|
+
* a cell you own, flip the cell public, and the bytes serve to the world,
|
|
48
|
+
* while a 200 against a 404 says whether the fact exists. What closes that is
|
|
49
|
+
* **possession-gated ref admission** on the cell verbs
|
|
50
|
+
* (`auth/cell_ref_admission.ts`): a non-admin `cell_create` / `cell_update` /
|
|
51
|
+
* `cell_clone` may introduce a ref only to a fact its acting actor holds —
|
|
52
|
+
* one it put the bytes of, recorded in `fact_holder` on every put, a
|
|
53
|
+
* deduplicated put included — and is refused `403 cell_ref_not_held`
|
|
54
|
+
* otherwise, the same answer whether or not a fact is stored. Refs a cell
|
|
55
|
+
* already carries stay admitted through updates and clones, so a viewer who
|
|
56
|
+
* clones a cell carries its refs: what a viewer can read through a cell, it
|
|
57
|
+
* can republish — no more than downloading the bytes and uploading them
|
|
58
|
+
* again. A serve is a snapshot of the edge at read time; revoking someone's
|
|
59
|
+
* access to a cell does not withdraw refs they already wrote elsewhere.
|
|
60
|
+
* Writers that bypass the verbs (seeds, raw inserts) are trusted and not
|
|
61
|
+
* admitted.
|
|
62
|
+
*
|
|
44
63
|
* The bare-hash route is **admin-only**: an admin's reach already spans
|
|
45
64
|
* every cell, so serving by bare hash grants no escalation. Non-admin
|
|
46
65
|
* callers are rejected at the auth phase and never reach the handler.
|
|
@@ -64,10 +83,11 @@
|
|
|
64
83
|
* don't distinguish 403 from 404 — neither the existence of a fact nor the
|
|
65
84
|
* existence of a cell→fact edge should leak through the public surface.
|
|
66
85
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
86
|
+
* A ref is a `data` string value that is exactly a fact hash
|
|
87
|
+
* (`derive_cell_refs`) — a hash inside prose or markup is not one. So a doc
|
|
88
|
+
* cell that shows images stores each image's hash as its own string value
|
|
89
|
+
* (an attachment list, say) beside the text that places them; serving then
|
|
90
|
+
* goes view-doc-cell → doc-cell-refs-include-hash → serve.
|
|
71
91
|
*
|
|
72
92
|
* ## Untrusted-content hardening
|
|
73
93
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkHG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAetD,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAO/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkEjD,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAkHD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SAuDF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SA2CF,CAAC"}
|
|
@@ -41,6 +41,25 @@
|
|
|
41
41
|
* hash. B publishing identical bytes from B's public cell makes them
|
|
42
42
|
* readable *via B's cell* — it never touches A's private reference.
|
|
43
43
|
*
|
|
44
|
+
* The edge is only as safe as what may create it. A cell's `refs` derive
|
|
45
|
+
* from its `data`, and any account that can write a cell writes its `data` —
|
|
46
|
+
* so this route alone would make a known hash a read capability: name it in
|
|
47
|
+
* a cell you own, flip the cell public, and the bytes serve to the world,
|
|
48
|
+
* while a 200 against a 404 says whether the fact exists. What closes that is
|
|
49
|
+
* **possession-gated ref admission** on the cell verbs
|
|
50
|
+
* (`auth/cell_ref_admission.ts`): a non-admin `cell_create` / `cell_update` /
|
|
51
|
+
* `cell_clone` may introduce a ref only to a fact its acting actor holds —
|
|
52
|
+
* one it put the bytes of, recorded in `fact_holder` on every put, a
|
|
53
|
+
* deduplicated put included — and is refused `403 cell_ref_not_held`
|
|
54
|
+
* otherwise, the same answer whether or not a fact is stored. Refs a cell
|
|
55
|
+
* already carries stay admitted through updates and clones, so a viewer who
|
|
56
|
+
* clones a cell carries its refs: what a viewer can read through a cell, it
|
|
57
|
+
* can republish — no more than downloading the bytes and uploading them
|
|
58
|
+
* again. A serve is a snapshot of the edge at read time; revoking someone's
|
|
59
|
+
* access to a cell does not withdraw refs they already wrote elsewhere.
|
|
60
|
+
* Writers that bypass the verbs (seeds, raw inserts) are trusted and not
|
|
61
|
+
* admitted.
|
|
62
|
+
*
|
|
44
63
|
* The bare-hash route is **admin-only**: an admin's reach already spans
|
|
45
64
|
* every cell, so serving by bare hash grants no escalation. Non-admin
|
|
46
65
|
* callers are rejected at the auth phase and never reach the handler.
|
|
@@ -64,10 +83,11 @@
|
|
|
64
83
|
* don't distinguish 403 from 404 — neither the existence of a fact nor the
|
|
65
84
|
* existence of a cell→fact edge should leak through the public surface.
|
|
66
85
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
86
|
+
* A ref is a `data` string value that is exactly a fact hash
|
|
87
|
+
* (`derive_cell_refs`) — a hash inside prose or markup is not one. So a doc
|
|
88
|
+
* cell that shows images stores each image's hash as its own string value
|
|
89
|
+
* (an attachment list, say) beside the text that places them; serving then
|
|
90
|
+
* goes view-doc-cell → doc-cell-refs-include-hash → serve.
|
|
71
91
|
*
|
|
72
92
|
* ## Untrusted-content hardening
|
|
73
93
|
*
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -968,7 +968,9 @@ source of truth for wire-shape conformance.
|
|
|
968
968
|
gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference +
|
|
969
969
|
admin-only bare-hash fact-serving parity suite); like cells it stays off the
|
|
970
970
|
declared surface and is `true` on every spine that mounts the serve routes +
|
|
971
|
-
the `_testing_put_fact` seeder
|
|
971
|
+
the `_testing_put_fact` seeder (whose optional `holder` records a
|
|
972
|
+
`fact_holder` row, so a non-admin can reference what it seeds) — it also
|
|
973
|
+
gates `describe_cell_ref_admission_cross_tests`. `ready` gates `describe_ready_cross_tests`
|
|
972
974
|
(anonymous `GET /ready` → `200 {ready: true}` on a clean spine bootstrap);
|
|
973
975
|
like cells/sse the `/ready` deploy gate stays off the declared surface, `true`
|
|
974
976
|
on every spine that live-mounts it over the shared `expected_schema.json`.
|
|
@@ -1607,7 +1609,8 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1607
1609
|
payload-expiry tests never reached. Daemon-token-gated like its siblings; the
|
|
1608
1610
|
Rust mirror is `fuz_testing::create_testing_mint_session_action_spec`.
|
|
1609
1611
|
|
|
1610
|
-
|
|
1612
|
+
In the hono-free sibling `testing/cross_backend/testing_origin_check.ts`,
|
|
1613
|
+
**not** bundled: `_testing_origin_check`
|
|
1611
1614
|
(`create_testing_origin_check_action()`, `run_testing_origin_check(input)`,
|
|
1612
1615
|
`TestingOriginCheckInput` / `TestingOriginCheckOutput`) — answers the origin
|
|
1613
1616
|
allowlist's parse and request check for one `{patterns, origin}` pair:
|
|
@@ -1617,7 +1620,8 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1617
1620
|
keeps an exact action-manifest gate; the spine binary appends it in
|
|
1618
1621
|
`build_full_spine_rpc_actions`.
|
|
1619
1622
|
|
|
1620
|
-
|
|
1623
|
+
Back in `testing_reset_actions.ts`, also **not** bundled:
|
|
1624
|
+
`_testing_shorten_credential_expiry`
|
|
1621
1625
|
(`create_testing_shorten_credential_expiry_action()`) — pulls an account's
|
|
1622
1626
|
`auth_session` and `api_token` expiries in to `seconds` (1–60) from now,
|
|
1623
1627
|
shorten-only (`LEAST`, an eternal token gaining one), answering
|
|
@@ -1628,8 +1632,8 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1628
1632
|
`_testing_origin_check`. The Rust mirror is
|
|
1629
1633
|
`fuz_testing::create_testing_shorten_credential_expiry_action_spec`.
|
|
1630
1634
|
|
|
1631
|
-
Also in
|
|
1632
|
-
`_testing_action_manifest` (+ `create_testing_action_manifest_action(mounted)`):
|
|
1635
|
+
Also in `testing_reset_actions.ts` — but **not** bundled by
|
|
1636
|
+
`create_testing_actions` — `_testing_action_manifest` (+ `create_testing_action_manifest_action(mounted)`):
|
|
1633
1637
|
dumps the live RPC registry as a normalized `ActionManifest` for the
|
|
1634
1638
|
cross-impl manifest-parity gate (see §Cross-impl RPC-manifest parity). It must
|
|
1635
1639
|
enumerate _every_ mounted method, which `create_testing_actions` (one
|
|
@@ -1638,6 +1642,28 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1638
1642
|
complete action list plus its own spec. Daemon-token-gated like its siblings;
|
|
1639
1643
|
the Rust mirror is `fuz_testing::create_testing_action_manifest_action_spec`.
|
|
1640
1644
|
|
|
1645
|
+
### Cell ref admission parity — `cross_backend/cell_ref_admission.ts`
|
|
1646
|
+
|
|
1647
|
+
`describe_cell_ref_admission_cross_tests({setup_test, capabilities, rpc_path?})`
|
|
1648
|
+
— possession-gated ref admission over real HTTP: a non-admin `cell_create`
|
|
1649
|
+
naming a stored fact it never put and one naming an unstored hash get one
|
|
1650
|
+
identical `403 cell_ref_not_held` (status + error body); a system-put fact is
|
|
1651
|
+
refused too; the uploader's public cell serves the bytes to anyone; a second
|
|
1652
|
+
actor putting identical bytes becomes a holder; an editor keeps refs another
|
|
1653
|
+
writer introduced and is refused adding (or restoring a dropped) unheld one; a
|
|
1654
|
+
shallow and a deep clone carry each row's source refs, while a patch adding an
|
|
1655
|
+
unheld ref or moving a child's ref onto the root is refused; the keeper (a
|
|
1656
|
+
global admin) is ungated; a grantee who read a fact through a granted cell
|
|
1657
|
+
can't name its hash in a cell of its own, before or after the revoke; the ref
|
|
1658
|
+
grammar is exact (prose, markup, `blake3:zz`, uppercase, object keys are not
|
|
1659
|
+
refs); and a hidden cell's update, a create under a hidden parent, and a clone
|
|
1660
|
+
of a hidden source answer `404` whatever `data` names. Facts are put through
|
|
1661
|
+
`cross_put_fact` (exported by `fact_serving.ts`), whose `holder` maps onto
|
|
1662
|
+
`_testing_put_fact`'s optional `holder` key. Gated on
|
|
1663
|
+
`capabilities.fact_serving`; cross-process only. fuz_app's own wiring is
|
|
1664
|
+
`src/test/cross_backend/cell_ref_admission.cross.test.ts`; the in-process
|
|
1665
|
+
cases are `src/test/auth/cell_ref_admission{,.fact_less}.db.test.ts`.
|
|
1666
|
+
|
|
1641
1667
|
### Token-scope surface parity — `cross_backend/token_scope_surface.ts`
|
|
1642
1668
|
|
|
1643
1669
|
`describe_token_scope_surface_cross_tests({setup_test, capabilities, rpc_path?,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,
|
|
1
|
+
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IAggBF,CAAC"}
|
|
@@ -100,14 +100,17 @@ export const describe_cell_crud_cross_tests = (options) => {
|
|
|
100
100
|
});
|
|
101
101
|
test_if(capabilities.cell_crud, 'refs are deduped and lexicographically sorted (deterministic, cross-impl-identical)', async () => {
|
|
102
102
|
const fixture = await setup_test();
|
|
103
|
-
const owner = await fixture.create_account({ username: 'cell_refs_owner' });
|
|
104
103
|
const t = fixture.fresh_transport();
|
|
105
|
-
|
|
104
|
+
// The keeper — a global admin, whom ref admission doesn't gate — so
|
|
105
|
+
// fabricated hashes nobody holds are writable refs (a non-admin
|
|
106
|
+
// writing them is `cell_ref_not_held`, the admission suite's
|
|
107
|
+
// subject), and the case runs on backends without the fact chain.
|
|
108
|
+
const owner_headers = fixture.create_session_headers();
|
|
106
109
|
// Three valid distinct fact hashes that sort a < b < c, embedded in
|
|
107
110
|
// `data` in a deliberately unsorted, nested, duplicated order. Both
|
|
108
111
|
// spines must derive `refs` as the sorted [a, b, c] regardless of
|
|
109
112
|
// traversal order or `HashSet` iteration — the byte-identical
|
|
110
|
-
// cross-backend contract (Rust `extract_refs` ↔ TS `
|
|
113
|
+
// cross-backend contract (Rust `extract_refs` ↔ TS `derive_cell_refs`).
|
|
111
114
|
const ref_a = `blake3:${'a'.repeat(64)}`;
|
|
112
115
|
const ref_b = `blake3:${'b'.repeat(64)}`;
|
|
113
116
|
const ref_c = `blake3:${'c'.repeat(64)}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoE9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAqB1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAuTF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,
|
|
1
|
+
{"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoE9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAqB1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAuTF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,IAs6BF,CAAC"}
|