@tangle-network/agent-app 0.44.19 → 0.44.22
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/forms/index.d.ts +333 -0
- package/dist/forms/index.js +343 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/teams-react/index.js +3 -3
- package/package.json +20 -10
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { PDFForm } from 'pdf-lib';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The blank a form is filled from — embedded, pinned, and never fetched at
|
|
5
|
+
* fill time.
|
|
6
|
+
*
|
|
7
|
+
* A renderer that downloads its own blank works on a developer laptop and
|
|
8
|
+
* fails in both places these products actually run: a Cloudflare Worker has no
|
|
9
|
+
* business making an outbound call mid-request, and a sandbox container's
|
|
10
|
+
* egress proxy refuses the agencies' own hosts (measured on tax-agent:
|
|
11
|
+
* `www.irs.gov` CONNECT tunnel 403 while pypi and npm returned 200). A
|
|
12
|
+
* form-filler that cannot reach its blank does not fail loudly — it degrades
|
|
13
|
+
* into an agent describing the form in prose, which is the exact behaviour
|
|
14
|
+
* this module exists to end.
|
|
15
|
+
*
|
|
16
|
+
* Pinning the bytes by digest also pins the artifact: a filing made against
|
|
17
|
+
* the 2025 revision is reproducible from the committed bytes, and an agency
|
|
18
|
+
* revision shows up as a diff of the recorded checksum instead of silently
|
|
19
|
+
* changing under a live URL.
|
|
20
|
+
*/
|
|
21
|
+
/** A blank form's bytes, base64-encoded, with the provenance to check them. */
|
|
22
|
+
interface FormBlank {
|
|
23
|
+
/** Base64 of the PDF exactly as the agency published it. */
|
|
24
|
+
base64: string;
|
|
25
|
+
/** SHA-256 of the decoded bytes, lowercase hex. */
|
|
26
|
+
sha256: string;
|
|
27
|
+
/** Where the bytes came from. Provenance for a reviewer, not a fetch target. */
|
|
28
|
+
sourceUrl: string;
|
|
29
|
+
/** Decoded length in bytes. A cheap first check that the base64 is intact. */
|
|
30
|
+
byteLength: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Decode a blank once per isolate.
|
|
34
|
+
*
|
|
35
|
+
* Keyed on the blank OBJECT rather than a module-level singleton, because a
|
|
36
|
+
* product carries several forms and a single cached slot would serve one
|
|
37
|
+
* form's bytes for another's fill — a failure that produces a plausible PDF
|
|
38
|
+
* and no error at all.
|
|
39
|
+
*/
|
|
40
|
+
declare function decodeFormBlank(blank: FormBlank): Uint8Array;
|
|
41
|
+
/** Lowercase-hex SHA-256 of a byte range. */
|
|
42
|
+
declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Prove the embedded bytes are the file the registry was derived from.
|
|
45
|
+
*
|
|
46
|
+
* Run this in a test, not on the fill path: a registry is only meaningful
|
|
47
|
+
* against the exact revision it was derived from, and a blank swapped for a
|
|
48
|
+
* newer revision moves every widget without changing a single field name.
|
|
49
|
+
*/
|
|
50
|
+
declare function assertFormBlankIntegrity(blank: FormBlank): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The slot → widget registry: the one place a form filler can be wrong
|
|
54
|
+
* invisibly, and the one place a language model is never allowed to write.
|
|
55
|
+
*
|
|
56
|
+
* WHY THE MODEL NEVER NAMES A WIDGET
|
|
57
|
+
*
|
|
58
|
+
* An agency PDF's widget names carry no meaning a reader can check —
|
|
59
|
+
* `topmostSubform[0].Page2[0].f2_06[0]` on an IRS form, `registered2` on a
|
|
60
|
+
* Texas SOS form. A model asked for one produces a plausible string, and the
|
|
61
|
+
* natural audit — read the field back by the name you just wrote — PASSES
|
|
62
|
+
* REGARDLESS, because it re-reads the invention rather than checking it.
|
|
63
|
+
* Measured on tax-agent: three values written to model-chosen widgets landed
|
|
64
|
+
* in "Combat zone" and the date boxes, and the audit reported 3 passed /
|
|
65
|
+
* 0 failed.
|
|
66
|
+
*
|
|
67
|
+
* So the model supplies SEMANTIC SLOTS — a form line, a named box — and the
|
|
68
|
+
* platform owns the slot → widget mapping. A misplaced figure stops being an
|
|
69
|
+
* invention that survives review and becomes something that cannot be
|
|
70
|
+
* expressed at all.
|
|
71
|
+
*
|
|
72
|
+
* WHY A SLOT CARRIES A LABEL, AND WHY THE LABEL DECLARES ITS BASIS
|
|
73
|
+
*
|
|
74
|
+
* A registry is still just a table, and a hand-typed table drifts (tax-agent's
|
|
75
|
+
* form catalog carried wrong page counts for 7 of 17 forms). So every slot
|
|
76
|
+
* states the label it believes it is aiming at, and states where that belief
|
|
77
|
+
* came from:
|
|
78
|
+
*
|
|
79
|
+
* - `labelBasis: 'widget'` — the label must match the widget's OWN `/TU`
|
|
80
|
+
* accessibility text inside the PDF. `checkRegistryAgainstBlank` re-checks
|
|
81
|
+
* it against the bytes, so a mis-aimed slot fails a test rather than
|
|
82
|
+
* quietly printing in the wrong box. Every one of Texas Form 205's 64
|
|
83
|
+
* widgets carries `/TU`, so its whole registry can be checked this way.
|
|
84
|
+
* - `labelBasis: 'derived'` — the label came from somewhere else (an XFA
|
|
85
|
+
* template, a published instruction sheet, a human reading the form) and
|
|
86
|
+
* CANNOT be re-checked against the widget. It is evidence for a reviewer,
|
|
87
|
+
* never proof. Measured on IRS f1040: 0 of 199 widgets carry `/TU` or
|
|
88
|
+
* `/Alt`, so its labels can only ever be derived.
|
|
89
|
+
*
|
|
90
|
+
* Declaring the basis is the point. A guess presented as truth is how a second
|
|
91
|
+
* self-confirming artifact gets built on top of the first one.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/** What kind of widget a slot writes to. */
|
|
95
|
+
type FormSlotKind = 'text' | 'checkbox';
|
|
96
|
+
/** How a slot's text value is rendered into the box. */
|
|
97
|
+
type FormSlotFormat = 'text' | 'currency';
|
|
98
|
+
/** Where a slot's `label` came from, and therefore what it can prove. */
|
|
99
|
+
type FormLabelBasis = 'widget' | 'derived';
|
|
100
|
+
/** One semantic slot: what a caller supplies, and where the platform puts it. */
|
|
101
|
+
interface FormSlot {
|
|
102
|
+
/** The name a caller (and a model) uses: `15`, `entity_name`, `agent_is_org`. */
|
|
103
|
+
slot: string;
|
|
104
|
+
/**
|
|
105
|
+
* The AcroForm widget path(s) this slot writes.
|
|
106
|
+
*
|
|
107
|
+
* A LIST because one semantic value legitimately occupies several boxes: IRS
|
|
108
|
+
* Form 1040 states adjusted gross income twice, at the foot of page 1 and
|
|
109
|
+
* the head of page 2, so page 2's arithmetic stands alone. Writing only one
|
|
110
|
+
* of them leaves "subtract line 14 from line 11b" pointing at an empty box.
|
|
111
|
+
*/
|
|
112
|
+
fields: readonly string[];
|
|
113
|
+
kind: FormSlotKind;
|
|
114
|
+
/** How the value is rendered. Ignored for `checkbox`. Default `'text'`. */
|
|
115
|
+
format?: FormSlotFormat;
|
|
116
|
+
/** What this slot is, in the form's own words. */
|
|
117
|
+
label: string;
|
|
118
|
+
/** Whether `label` is checkable against the PDF, or merely recorded. */
|
|
119
|
+
labelBasis: FormLabelBasis;
|
|
120
|
+
}
|
|
121
|
+
/** A form's complete slot table, pinned to the revision it was derived from. */
|
|
122
|
+
interface FormRegistry {
|
|
123
|
+
/** Stable id for the form: `us-irs-1040`, `us-tx-sos-205`. */
|
|
124
|
+
form: string;
|
|
125
|
+
/** The agency's own revision marker: `2025`, `Rev. 12-21`. */
|
|
126
|
+
revision: string;
|
|
127
|
+
slots: readonly FormSlot[];
|
|
128
|
+
}
|
|
129
|
+
/** Why a registry does not match the blank it claims to describe. */
|
|
130
|
+
type RegistryProblemCode = 'no_slots' | 'no_fields' | 'missing_field' | 'wrong_kind' | 'label_mismatch' | 'duplicate_field' | 'duplicate_slot';
|
|
131
|
+
interface RegistryProblem {
|
|
132
|
+
slot: string;
|
|
133
|
+
field?: string;
|
|
134
|
+
code: RegistryProblemCode;
|
|
135
|
+
detail: string;
|
|
136
|
+
}
|
|
137
|
+
interface RegistryCheckResult {
|
|
138
|
+
ok: boolean;
|
|
139
|
+
/** Widgets actually compared against the PDF. Zero means nothing was proven. */
|
|
140
|
+
checked: number;
|
|
141
|
+
/** Slots whose label was compared against the widget's own `/TU`. */
|
|
142
|
+
labelsChecked: number;
|
|
143
|
+
problems: RegistryProblem[];
|
|
144
|
+
}
|
|
145
|
+
/** The `/TU` accessibility text a widget carries, if any. */
|
|
146
|
+
declare function widgetLabel(form: PDFForm, field: string): Promise<string | undefined>;
|
|
147
|
+
/**
|
|
148
|
+
* Check a registry against the blank it claims to describe.
|
|
149
|
+
*
|
|
150
|
+
* This is the placement check. It runs in a test or in CI, never on the fill
|
|
151
|
+
* path, and it is the ONLY thing that can catch a slot aimed at the wrong box
|
|
152
|
+
* — reading a value back after writing it cannot, because it re-reads the same
|
|
153
|
+
* name it wrote.
|
|
154
|
+
*
|
|
155
|
+
* It fails on ABSENCE, deliberately. tax-agent shipped an audit that silently
|
|
156
|
+
* skipped an expected field the PDF did not expose, so a fill against entirely
|
|
157
|
+
* wrong paths reported `passed=0 failed=0`. Here an empty registry, a slot
|
|
158
|
+
* with no fields, and a field the PDF does not expose are each a problem with
|
|
159
|
+
* a name, and `checked` is reported so a caller can see how much was actually
|
|
160
|
+
* proven rather than trusting a bare `ok: true`.
|
|
161
|
+
*/
|
|
162
|
+
declare function checkRegistryAgainstBlank(args: {
|
|
163
|
+
/** The blank's bytes. Decode a `FormBlank` with `decodeFormBlank` first. */
|
|
164
|
+
pdf: Uint8Array;
|
|
165
|
+
registry: FormRegistry;
|
|
166
|
+
}): Promise<RegistryCheckResult>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Fill a real agency PDF from a slot → value map.
|
|
170
|
+
*
|
|
171
|
+
* Mechanically this is `pdf-lib` writing an AcroForm: TypeScript, no
|
|
172
|
+
* container, no Python, no network. That matters because it is the only shape
|
|
173
|
+
* that runs everywhere these products run — a Cloudflare Worker (where 100% of
|
|
174
|
+
* tax-agent's production work products were produced, with no sandbox at all)
|
|
175
|
+
* and a sandbox container whose egress proxy refuses the agency's own host.
|
|
176
|
+
*
|
|
177
|
+
* The invariant it enforces is in `registry.ts`: the caller supplies SEMANTIC
|
|
178
|
+
* SLOTS and never a widget name. Two consequences show up here.
|
|
179
|
+
*
|
|
180
|
+
* NOTHING IS SILENT. Every slot ends in `filled` or in `unfilled` with a code
|
|
181
|
+
* and a reason. A value the caller supplied that never reached the page means
|
|
182
|
+
* the document and the data disagree, which is the one thing a reviewer must
|
|
183
|
+
* not have to discover by eye. That includes a registry naming a widget the
|
|
184
|
+
* PDF does not expose: it is reported per-slot rather than crashing the render
|
|
185
|
+
* or — worse — being skipped, which is how tax-agent once audited a fill
|
|
186
|
+
* against entirely wrong paths as `passed=0 failed=0`.
|
|
187
|
+
*
|
|
188
|
+
* NO ON-STATE IS EVER AUTHORED. A checkbox's "on" name is a property of the
|
|
189
|
+
* PDF and frequently a hex-escaped sentence: Texas Form 205's seven boxes are
|
|
190
|
+
* `is#20an#20organization`, `initially#20has#20#20managers` (note the double
|
|
191
|
+
* space), and four more like them. A caller that types one of those strings is
|
|
192
|
+
* authoring something it cannot check, so a checkbox slot takes a BOOLEAN and
|
|
193
|
+
* the widget's own on-value is read out of the file. The escaped spelling is
|
|
194
|
+
* not handled — it is unrepresentable.
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
/** One widget the fill actually wrote. */
|
|
198
|
+
interface FilledWidget {
|
|
199
|
+
slot: string;
|
|
200
|
+
field: string;
|
|
201
|
+
kind: FormSlot['kind'];
|
|
202
|
+
/** The value as the caller supplied it, before formatting. */
|
|
203
|
+
value: unknown;
|
|
204
|
+
/** Exactly the text placed in the box. Absent for a checkbox. */
|
|
205
|
+
text?: string;
|
|
206
|
+
/** Whether a checkbox was ticked. Absent for a text field. */
|
|
207
|
+
checked?: boolean;
|
|
208
|
+
/** The widget's OWN on-state, decoded — read from the PDF, never authored. */
|
|
209
|
+
onState?: string;
|
|
210
|
+
}
|
|
211
|
+
/** Why a supplied value did not reach the page. */
|
|
212
|
+
type UnfilledCode = 'unknown_slot' | 'missing_field' | 'wrong_kind' | 'not_a_number' | 'not_a_boolean' | 'unformattable';
|
|
213
|
+
interface UnfilledSlot {
|
|
214
|
+
slot: string;
|
|
215
|
+
field?: string;
|
|
216
|
+
value: unknown;
|
|
217
|
+
code: UnfilledCode;
|
|
218
|
+
reason: string;
|
|
219
|
+
}
|
|
220
|
+
interface FillFormResult {
|
|
221
|
+
bytes: Uint8Array;
|
|
222
|
+
filled: FilledWidget[];
|
|
223
|
+
unfilled: UnfilledSlot[];
|
|
224
|
+
form: string;
|
|
225
|
+
revision: string;
|
|
226
|
+
}
|
|
227
|
+
interface FillFormOptions {
|
|
228
|
+
/** The blank's bytes. Decode a `FormBlank` with `decodeFormBlank` first. */
|
|
229
|
+
pdf: Uint8Array;
|
|
230
|
+
registry: FormRegistry;
|
|
231
|
+
/** slot name → value. Keys the registry does not know are reported, not dropped. */
|
|
232
|
+
values: Record<string, unknown>;
|
|
233
|
+
/**
|
|
234
|
+
* Override how a text value becomes box text. Return `undefined` to fall
|
|
235
|
+
* through to the built-in formatting for the slot's `format`.
|
|
236
|
+
*/
|
|
237
|
+
formatText?: (value: unknown, slot: FormSlot) => string | undefined;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Format a figure the way a US agency form prints it: grouped thousands, two
|
|
241
|
+
* decimals, negatives in parentheses.
|
|
242
|
+
*
|
|
243
|
+
* Two decimals rather than whole dollars because the caller's data carries
|
|
244
|
+
* cents and a reviewer compares the document against that data — rounding here
|
|
245
|
+
* would manufacture a disagreement between the two on every line with cents.
|
|
246
|
+
*/
|
|
247
|
+
declare function formatFormCurrency(value: number): string;
|
|
248
|
+
/** A number, or a number written the way a form prints one (`$141,318.74`). */
|
|
249
|
+
declare function parseFormAmount(value: unknown): number | undefined;
|
|
250
|
+
/**
|
|
251
|
+
* A checkbox takes a boolean and nothing else.
|
|
252
|
+
*
|
|
253
|
+
* Strict on purpose. Accepting a truthy string would accept the widget's own
|
|
254
|
+
* escaped on-state (`'is#20an#20organization'`) as "true", which is exactly
|
|
255
|
+
* the authored-on-state this module refuses — and it would accept `'no'` as
|
|
256
|
+
* true, silently ticking a box the caller meant to leave clear.
|
|
257
|
+
*/
|
|
258
|
+
declare function parseFormBoolean(value: unknown): boolean | undefined;
|
|
259
|
+
/**
|
|
260
|
+
* Write a value map onto a blank, returning the bytes plus the exact
|
|
261
|
+
* widget-by-widget account of what was and was not placed.
|
|
262
|
+
*/
|
|
263
|
+
declare function fillPdfForm(options: FillFormOptions): Promise<FillFormResult>;
|
|
264
|
+
/**
|
|
265
|
+
* What to tell the agent after a fill.
|
|
266
|
+
*
|
|
267
|
+
* Names the values that did NOT reach the page. Those are exactly the cases
|
|
268
|
+
* where the document and the data disagree, and the agent is the only party
|
|
269
|
+
* that can resolve it — returning a bare "done" hands a reviewer a form
|
|
270
|
+
* missing values the data claims are on it.
|
|
271
|
+
*/
|
|
272
|
+
declare function describeFormFill(result: FillFormResult): string;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Read a filled form back and say, slot by slot, whether the document agrees
|
|
276
|
+
* with the data it was built from.
|
|
277
|
+
*
|
|
278
|
+
* WHAT THIS PROVES, AND WHAT IT CANNOT
|
|
279
|
+
*
|
|
280
|
+
* This proves the WRITE LANDED: the value reached a real widget, that widget
|
|
281
|
+
* exists in the produced file, and it holds the text or tick the data claims.
|
|
282
|
+
* It does NOT prove PLACEMENT — that the widget is the right box on the page —
|
|
283
|
+
* because it resolves the same field names the fill used. Reading a value back
|
|
284
|
+
* by the name you just wrote passes even when the name was invented; measured
|
|
285
|
+
* on tax-agent, an audit of that shape reported 3 passed / 0 failed for three
|
|
286
|
+
* figures sitting in "Combat zone" and the date boxes.
|
|
287
|
+
*
|
|
288
|
+
* Placement is proven by `checkRegistryAgainstBlank`, which compares each
|
|
289
|
+
* slot's claimed label against the widget's own `/TU` text inside the PDF.
|
|
290
|
+
* The two checks are complements, and a product that runs only this one has
|
|
291
|
+
* the audit that already failed once. That is stated here rather than in a
|
|
292
|
+
* commit message because a future caller will otherwise reach for the
|
|
293
|
+
* convenient half.
|
|
294
|
+
*
|
|
295
|
+
* FAILS ON ABSENCE. A registry field the produced PDF does not expose is
|
|
296
|
+
* `missing_field`, and an empty `expected` map is `ok: false` — the
|
|
297
|
+
* `passed=0 failed=0` verdict is the exact shape of the bug this replaces.
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
type SlotVerdict = 'ok' | 'unknown_slot' | 'missing_field' | 'wrong_kind' | 'not_written' | 'mismatch';
|
|
301
|
+
/** What the slot's `label` is worth, checked against the produced file. */
|
|
302
|
+
type LabelVerdict = 'matches_widget' | 'label_mismatch' | 'derived_unchecked' | 'no_widget_label';
|
|
303
|
+
interface SlotVerification {
|
|
304
|
+
slot: string;
|
|
305
|
+
field?: string;
|
|
306
|
+
verdict: SlotVerdict;
|
|
307
|
+
/** What the data says the box should hold. */
|
|
308
|
+
expected?: string;
|
|
309
|
+
/** What the box actually holds, read out of the produced bytes. */
|
|
310
|
+
actual?: string;
|
|
311
|
+
labelVerdict?: LabelVerdict;
|
|
312
|
+
label?: string;
|
|
313
|
+
}
|
|
314
|
+
interface VerifyFormResult {
|
|
315
|
+
ok: boolean;
|
|
316
|
+
/** Widgets actually read back. Zero means nothing was proven. */
|
|
317
|
+
verified: number;
|
|
318
|
+
slots: SlotVerification[];
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Verify a filled form against the value map it was filled from.
|
|
322
|
+
*
|
|
323
|
+
* `expected` is the SAME shape passed to `fillPdfForm` — deliberately, so a
|
|
324
|
+
* caller cannot verify against a convenient restatement of what it wrote.
|
|
325
|
+
*/
|
|
326
|
+
declare function verifyFilledForm(args: {
|
|
327
|
+
/** The PRODUCED bytes, not the blank. */
|
|
328
|
+
pdf: Uint8Array;
|
|
329
|
+
registry: FormRegistry;
|
|
330
|
+
expected: Record<string, unknown>;
|
|
331
|
+
}): Promise<VerifyFormResult>;
|
|
332
|
+
|
|
333
|
+
export { type FillFormOptions, type FillFormResult, type FilledWidget, type FormBlank, type FormLabelBasis, type FormRegistry, type FormSlot, type FormSlotFormat, type FormSlotKind, type LabelVerdict, type RegistryCheckResult, type RegistryProblem, type RegistryProblemCode, type SlotVerdict, type SlotVerification, type UnfilledCode, type UnfilledSlot, type VerifyFormResult, assertFormBlankIntegrity, checkRegistryAgainstBlank, decodeFormBlank, describeFormFill, fillPdfForm, formatFormCurrency, parseFormAmount, parseFormBoolean, sha256Hex, verifyFilledForm, widgetLabel };
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// src/forms/blank.ts
|
|
2
|
+
var decoded = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function decodeFormBlank(blank) {
|
|
4
|
+
const cached = decoded.get(blank);
|
|
5
|
+
if (cached) return cached;
|
|
6
|
+
const binary = atob(blank.base64);
|
|
7
|
+
const bytes = new Uint8Array(binary.length);
|
|
8
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
9
|
+
if (bytes.length !== blank.byteLength) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`blank form is ${bytes.length} bytes but declares ${blank.byteLength} \u2014 the embedded base64 is truncated`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
decoded.set(blank, bytes);
|
|
15
|
+
return bytes;
|
|
16
|
+
}
|
|
17
|
+
async function sha256Hex(bytes) {
|
|
18
|
+
const view = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
19
|
+
const digest = await crypto.subtle.digest("SHA-256", view);
|
|
20
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
21
|
+
}
|
|
22
|
+
async function assertFormBlankIntegrity(blank) {
|
|
23
|
+
const bytes = decodeFormBlank(blank);
|
|
24
|
+
const digest = await sha256Hex(bytes);
|
|
25
|
+
if (digest !== blank.sha256) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`blank form digest is ${digest} but the registry was derived against ${blank.sha256} (${blank.sourceUrl})`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/forms/registry.ts
|
|
33
|
+
async function widgetLabel(form, field) {
|
|
34
|
+
const { PDFName, PDFHexString, PDFString } = await import("pdf-lib");
|
|
35
|
+
const target = form.getFieldMaybe(field);
|
|
36
|
+
if (!target) return void 0;
|
|
37
|
+
const tooltip = target.acroField.dict.get(PDFName.of("TU"));
|
|
38
|
+
if (tooltip instanceof PDFString || tooltip instanceof PDFHexString) return tooltip.decodeText();
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
function normalizeLabel(value) {
|
|
42
|
+
return value.replace(/\s+/gu, " ").trim().toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
async function checkRegistryAgainstBlank(args) {
|
|
45
|
+
const { PDFDocument } = await import("pdf-lib");
|
|
46
|
+
const document = await PDFDocument.load(args.pdf, { updateMetadata: false });
|
|
47
|
+
const form = document.getForm();
|
|
48
|
+
const exposed = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const field of form.getFields()) exposed.set(field.getName(), field.constructor.name);
|
|
50
|
+
const problems = [];
|
|
51
|
+
const claimedBy = /* @__PURE__ */ new Map();
|
|
52
|
+
const seenSlots = /* @__PURE__ */ new Set();
|
|
53
|
+
let checked = 0;
|
|
54
|
+
let labelsChecked = 0;
|
|
55
|
+
if (args.registry.slots.length === 0) {
|
|
56
|
+
problems.push({
|
|
57
|
+
slot: "(registry)",
|
|
58
|
+
code: "no_slots",
|
|
59
|
+
detail: `registry ${args.registry.form} declares no slots \u2014 it can prove nothing and fill nothing`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const slot of args.registry.slots) {
|
|
63
|
+
if (seenSlots.has(slot.slot)) {
|
|
64
|
+
problems.push({
|
|
65
|
+
slot: slot.slot,
|
|
66
|
+
code: "duplicate_slot",
|
|
67
|
+
detail: `slot ${slot.slot} is declared twice; the later entry silently wins at fill time`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
seenSlots.add(slot.slot);
|
|
71
|
+
if (slot.fields.length === 0) {
|
|
72
|
+
problems.push({
|
|
73
|
+
slot: slot.slot,
|
|
74
|
+
code: "no_fields",
|
|
75
|
+
detail: `slot ${slot.slot} names no widget, so a value supplied for it goes nowhere`
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const field of slot.fields) {
|
|
80
|
+
const previous = claimedBy.get(field);
|
|
81
|
+
if (previous !== void 0 && previous !== slot.slot) {
|
|
82
|
+
problems.push({
|
|
83
|
+
slot: slot.slot,
|
|
84
|
+
field,
|
|
85
|
+
code: "duplicate_field",
|
|
86
|
+
detail: `${field} is claimed by both slot ${previous} and slot ${slot.slot}`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
claimedBy.set(field, slot.slot);
|
|
90
|
+
const actualKind = exposed.get(field);
|
|
91
|
+
if (actualKind === void 0) {
|
|
92
|
+
problems.push({
|
|
93
|
+
slot: slot.slot,
|
|
94
|
+
field,
|
|
95
|
+
code: "missing_field",
|
|
96
|
+
detail: `${args.registry.form} has no widget named ${field}`
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
checked += 1;
|
|
101
|
+
const expectedKind = slot.kind === "checkbox" ? "PDFCheckBox" : "PDFTextField";
|
|
102
|
+
if (actualKind !== expectedKind) {
|
|
103
|
+
problems.push({
|
|
104
|
+
slot: slot.slot,
|
|
105
|
+
field,
|
|
106
|
+
code: "wrong_kind",
|
|
107
|
+
detail: `${field} is a ${actualKind}, but slot ${slot.slot} declares ${slot.kind}`
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (slot.labelBasis !== "widget") continue;
|
|
112
|
+
const onWidget = await widgetLabel(form, field);
|
|
113
|
+
if (onWidget === void 0) {
|
|
114
|
+
problems.push({
|
|
115
|
+
slot: slot.slot,
|
|
116
|
+
field,
|
|
117
|
+
code: "label_mismatch",
|
|
118
|
+
detail: `slot ${slot.slot} claims labelBasis 'widget' but ${field} carries no /TU text to check it against \u2014 the label is derived, not checked`
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
labelsChecked += 1;
|
|
123
|
+
if (normalizeLabel(onWidget) !== normalizeLabel(slot.label)) {
|
|
124
|
+
problems.push({
|
|
125
|
+
slot: slot.slot,
|
|
126
|
+
field,
|
|
127
|
+
code: "label_mismatch",
|
|
128
|
+
detail: `${field} is labelled ${JSON.stringify(onWidget)} but slot ${slot.slot} claims ${JSON.stringify(slot.label)}`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { ok: problems.length === 0, checked, labelsChecked, problems };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/forms/fill.ts
|
|
137
|
+
function formatFormCurrency(value) {
|
|
138
|
+
const magnitude = Math.abs(value).toLocaleString("en-US", {
|
|
139
|
+
minimumFractionDigits: 2,
|
|
140
|
+
maximumFractionDigits: 2
|
|
141
|
+
});
|
|
142
|
+
return value < 0 ? `(${magnitude})` : magnitude;
|
|
143
|
+
}
|
|
144
|
+
function parseFormAmount(value) {
|
|
145
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
146
|
+
if (typeof value !== "string") return void 0;
|
|
147
|
+
const normalized = value.replace(/[$,\s]/gu, "");
|
|
148
|
+
if (!/^-?\d+(?:\.\d+)?$/u.test(normalized)) return void 0;
|
|
149
|
+
const parsed = Number(normalized);
|
|
150
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
151
|
+
}
|
|
152
|
+
function parseFormBoolean(value) {
|
|
153
|
+
if (typeof value === "boolean") return value;
|
|
154
|
+
if (typeof value !== "string") return void 0;
|
|
155
|
+
const normalized = value.trim().toLowerCase();
|
|
156
|
+
if (normalized === "true" || normalized === "yes") return true;
|
|
157
|
+
if (normalized === "false" || normalized === "no") return false;
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
function textFor(value, slot, override) {
|
|
161
|
+
const custom = override?.(value, slot);
|
|
162
|
+
if (custom !== void 0) return { text: custom };
|
|
163
|
+
if ((slot.format ?? "text") === "currency") {
|
|
164
|
+
const amount = parseFormAmount(value);
|
|
165
|
+
if (amount === void 0) return { code: "not_a_number", reason: "not a numeric amount" };
|
|
166
|
+
return { text: formatFormCurrency(amount) };
|
|
167
|
+
}
|
|
168
|
+
if (typeof value === "string") return { text: value };
|
|
169
|
+
if (typeof value === "number" && Number.isFinite(value)) return { text: String(value) };
|
|
170
|
+
return { code: "unformattable", reason: `cannot render a ${typeof value} into a text box` };
|
|
171
|
+
}
|
|
172
|
+
async function fillPdfForm(options) {
|
|
173
|
+
const { PDFCheckBox: CheckBox, PDFDocument, PDFName, PDFTextField: TextField } = await import("pdf-lib");
|
|
174
|
+
const document = await PDFDocument.load(options.pdf, { updateMetadata: false });
|
|
175
|
+
const form = document.getForm();
|
|
176
|
+
if (document.catalog.getOrCreateAcroForm().dict.has(PDFName.of("XFA"))) {
|
|
177
|
+
throw new Error("filled form still carries an XFA layer \u2014 a viewer would draw the blank instead");
|
|
178
|
+
}
|
|
179
|
+
const bySlot = new Map(options.registry.slots.map((slot) => [slot.slot, slot]));
|
|
180
|
+
const filled = [];
|
|
181
|
+
const unfilled = [];
|
|
182
|
+
for (const [name, value] of Object.entries(options.values)) {
|
|
183
|
+
const slot = bySlot.get(name);
|
|
184
|
+
if (!slot) {
|
|
185
|
+
unfilled.push({
|
|
186
|
+
slot: name,
|
|
187
|
+
value,
|
|
188
|
+
code: "unknown_slot",
|
|
189
|
+
reason: `${options.registry.form} has no slot named ${name}`
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (slot.kind === "checkbox") {
|
|
194
|
+
const checked = parseFormBoolean(value);
|
|
195
|
+
if (checked === void 0) {
|
|
196
|
+
unfilled.push({
|
|
197
|
+
slot: name,
|
|
198
|
+
value,
|
|
199
|
+
code: "not_a_boolean",
|
|
200
|
+
reason: "a checkbox takes true or false; a checkbox on-state is read from the PDF and is never supplied"
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for (const field of slot.fields) {
|
|
205
|
+
const widget = form.getFieldMaybe(field);
|
|
206
|
+
if (!widget) {
|
|
207
|
+
unfilled.push({ slot: name, field, value, code: "missing_field", reason: `${options.registry.form} has no widget named ${field}` });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (!(widget instanceof CheckBox)) {
|
|
211
|
+
unfilled.push({ slot: name, field, value, code: "wrong_kind", reason: `${field} is a ${widget.constructor.name}, not a checkbox` });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const box = widget;
|
|
215
|
+
const onState = box.acroField.getOnValue()?.decodeText();
|
|
216
|
+
if (checked) box.check();
|
|
217
|
+
else box.uncheck();
|
|
218
|
+
filled.push({ slot: name, field, kind: "checkbox", value, checked, onState });
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const rendered = textFor(value, slot, options.formatText);
|
|
223
|
+
if ("code" in rendered) {
|
|
224
|
+
unfilled.push({ slot: name, value, code: rendered.code, reason: rendered.reason });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
for (const field of slot.fields) {
|
|
228
|
+
const widget = form.getFieldMaybe(field);
|
|
229
|
+
if (!widget) {
|
|
230
|
+
unfilled.push({ slot: name, field, value, code: "missing_field", reason: `${options.registry.form} has no widget named ${field}` });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!(widget instanceof TextField)) {
|
|
234
|
+
unfilled.push({ slot: name, field, value, code: "wrong_kind", reason: `${field} is a ${widget.constructor.name}, not a text field` });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
;
|
|
238
|
+
widget.setText(rendered.text);
|
|
239
|
+
filled.push({ slot: name, field, kind: "text", value, text: rendered.text });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
bytes: await document.save(),
|
|
244
|
+
filled,
|
|
245
|
+
unfilled,
|
|
246
|
+
form: options.registry.form,
|
|
247
|
+
revision: options.registry.revision
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function describeFormFill(result) {
|
|
251
|
+
const count = result.filled.length;
|
|
252
|
+
const base = `Filled ${result.form} (${result.revision}) with ${count} ${count === 1 ? "value" : "values"}.`;
|
|
253
|
+
if (result.unfilled.length === 0) return base;
|
|
254
|
+
const detail = result.unfilled.map((entry) => `${entry.slot} (${entry.reason})`).join("; ");
|
|
255
|
+
return `${base} NOT placed on the form: ${detail}. Those values are in the data but not on the document a reviewer opens.`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/forms/verify.ts
|
|
259
|
+
function expectedText(value, format) {
|
|
260
|
+
if (format === "currency") {
|
|
261
|
+
const amount = parseFormAmount(value);
|
|
262
|
+
return amount === void 0 ? String(value) : formatFormCurrency(amount);
|
|
263
|
+
}
|
|
264
|
+
return typeof value === "number" ? String(value) : String(value);
|
|
265
|
+
}
|
|
266
|
+
async function verifyFilledForm(args) {
|
|
267
|
+
const { PDFCheckBox: CheckBox, PDFDocument, PDFTextField: TextField } = await import("pdf-lib");
|
|
268
|
+
const document = await PDFDocument.load(args.pdf, { updateMetadata: false });
|
|
269
|
+
const form = document.getForm();
|
|
270
|
+
const exposed = new Set(form.getFields().map((field) => field.getName()));
|
|
271
|
+
const bySlot = new Map(args.registry.slots.map((slot) => [slot.slot, slot]));
|
|
272
|
+
const slots = [];
|
|
273
|
+
let verified = 0;
|
|
274
|
+
let ok = true;
|
|
275
|
+
for (const [name, value] of Object.entries(args.expected)) {
|
|
276
|
+
const slot = bySlot.get(name);
|
|
277
|
+
if (!slot) {
|
|
278
|
+
slots.push({ slot: name, verdict: "unknown_slot" });
|
|
279
|
+
ok = false;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (const field of slot.fields) {
|
|
283
|
+
if (!exposed.has(field)) {
|
|
284
|
+
slots.push({ slot: name, field, verdict: "missing_field" });
|
|
285
|
+
ok = false;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const widget = form.getField(field);
|
|
289
|
+
const label = await widgetLabel(form, field);
|
|
290
|
+
const labelVerdict = slot.labelBasis === "derived" ? "derived_unchecked" : label === void 0 ? "no_widget_label" : label.replace(/\s+/gu, " ").trim().toLowerCase() === slot.label.replace(/\s+/gu, " ").trim().toLowerCase() ? "matches_widget" : "label_mismatch";
|
|
291
|
+
if (labelVerdict === "label_mismatch" || labelVerdict === "no_widget_label") ok = false;
|
|
292
|
+
if (slot.kind === "checkbox") {
|
|
293
|
+
if (!(widget instanceof CheckBox)) {
|
|
294
|
+
slots.push({ slot: name, field, verdict: "wrong_kind", label, labelVerdict });
|
|
295
|
+
ok = false;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const want2 = parseFormBoolean(value);
|
|
299
|
+
const got2 = widget.isChecked();
|
|
300
|
+
const verdict2 = want2 === void 0 ? "mismatch" : got2 === want2 ? "ok" : "mismatch";
|
|
301
|
+
if (verdict2 !== "ok") ok = false;
|
|
302
|
+
else verified += 1;
|
|
303
|
+
slots.push({
|
|
304
|
+
slot: name,
|
|
305
|
+
field,
|
|
306
|
+
verdict: verdict2,
|
|
307
|
+
expected: String(want2),
|
|
308
|
+
actual: String(got2),
|
|
309
|
+
label,
|
|
310
|
+
labelVerdict
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!(widget instanceof TextField)) {
|
|
315
|
+
slots.push({ slot: name, field, verdict: "wrong_kind", label, labelVerdict });
|
|
316
|
+
ok = false;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const want = expectedText(value, slot.format);
|
|
320
|
+
const got = widget.getText() ?? "";
|
|
321
|
+
const verdict = got === "" && want !== "" ? "not_written" : got === want ? "ok" : "mismatch";
|
|
322
|
+
if (verdict !== "ok") ok = false;
|
|
323
|
+
else verified += 1;
|
|
324
|
+
slots.push({ slot: name, field, verdict, expected: want, actual: got, label, labelVerdict });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (verified === 0) ok = false;
|
|
328
|
+
return { ok, verified, slots };
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
assertFormBlankIntegrity,
|
|
332
|
+
checkRegistryAgainstBlank,
|
|
333
|
+
decodeFormBlank,
|
|
334
|
+
describeFormFill,
|
|
335
|
+
fillPdfForm,
|
|
336
|
+
formatFormCurrency,
|
|
337
|
+
parseFormAmount,
|
|
338
|
+
parseFormBoolean,
|
|
339
|
+
sha256Hex,
|
|
340
|
+
verifyFilledForm,
|
|
341
|
+
widgetLabel
|
|
342
|
+
};
|
|
343
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/forms/blank.ts","../../src/forms/registry.ts","../../src/forms/fill.ts","../../src/forms/verify.ts"],"sourcesContent":["/**\n * The blank a form is filled from — embedded, pinned, and never fetched at\n * fill time.\n *\n * A renderer that downloads its own blank works on a developer laptop and\n * fails in both places these products actually run: a Cloudflare Worker has no\n * business making an outbound call mid-request, and a sandbox container's\n * egress proxy refuses the agencies' own hosts (measured on tax-agent:\n * `www.irs.gov` CONNECT tunnel 403 while pypi and npm returned 200). A\n * form-filler that cannot reach its blank does not fail loudly — it degrades\n * into an agent describing the form in prose, which is the exact behaviour\n * this module exists to end.\n *\n * Pinning the bytes by digest also pins the artifact: a filing made against\n * the 2025 revision is reproducible from the committed bytes, and an agency\n * revision shows up as a diff of the recorded checksum instead of silently\n * changing under a live URL.\n */\n\n/** A blank form's bytes, base64-encoded, with the provenance to check them. */\nexport interface FormBlank {\n /** Base64 of the PDF exactly as the agency published it. */\n base64: string\n /** SHA-256 of the decoded bytes, lowercase hex. */\n sha256: string\n /** Where the bytes came from. Provenance for a reviewer, not a fetch target. */\n sourceUrl: string\n /** Decoded length in bytes. A cheap first check that the base64 is intact. */\n byteLength: number\n}\n\nconst decoded = new WeakMap<FormBlank, Uint8Array>()\n\n/**\n * Decode a blank once per isolate.\n *\n * Keyed on the blank OBJECT rather than a module-level singleton, because a\n * product carries several forms and a single cached slot would serve one\n * form's bytes for another's fill — a failure that produces a plausible PDF\n * and no error at all.\n */\nexport function decodeFormBlank(blank: FormBlank): Uint8Array {\n const cached = decoded.get(blank)\n if (cached) return cached\n const binary = atob(blank.base64)\n const bytes = new Uint8Array(binary.length)\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)\n if (bytes.length !== blank.byteLength) {\n throw new Error(\n `blank form is ${bytes.length} bytes but declares ${blank.byteLength} — the embedded base64 is truncated`,\n )\n }\n decoded.set(blank, bytes)\n return bytes\n}\n\n/** Lowercase-hex SHA-256 of a byte range. */\nexport async function sha256Hex(bytes: Uint8Array): Promise<string> {\n const view = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)\n const digest = await crypto.subtle.digest('SHA-256', view as ArrayBuffer)\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Prove the embedded bytes are the file the registry was derived from.\n *\n * Run this in a test, not on the fill path: a registry is only meaningful\n * against the exact revision it was derived from, and a blank swapped for a\n * newer revision moves every widget without changing a single field name.\n */\nexport async function assertFormBlankIntegrity(blank: FormBlank): Promise<void> {\n const bytes = decodeFormBlank(blank)\n const digest = await sha256Hex(bytes)\n if (digest !== blank.sha256) {\n throw new Error(\n `blank form digest is ${digest} but the registry was derived against ${blank.sha256} (${blank.sourceUrl})`,\n )\n }\n}\n","/**\n * The slot → widget registry: the one place a form filler can be wrong\n * invisibly, and the one place a language model is never allowed to write.\n *\n * WHY THE MODEL NEVER NAMES A WIDGET\n *\n * An agency PDF's widget names carry no meaning a reader can check —\n * `topmostSubform[0].Page2[0].f2_06[0]` on an IRS form, `registered2` on a\n * Texas SOS form. A model asked for one produces a plausible string, and the\n * natural audit — read the field back by the name you just wrote — PASSES\n * REGARDLESS, because it re-reads the invention rather than checking it.\n * Measured on tax-agent: three values written to model-chosen widgets landed\n * in \"Combat zone\" and the date boxes, and the audit reported 3 passed /\n * 0 failed.\n *\n * So the model supplies SEMANTIC SLOTS — a form line, a named box — and the\n * platform owns the slot → widget mapping. A misplaced figure stops being an\n * invention that survives review and becomes something that cannot be\n * expressed at all.\n *\n * WHY A SLOT CARRIES A LABEL, AND WHY THE LABEL DECLARES ITS BASIS\n *\n * A registry is still just a table, and a hand-typed table drifts (tax-agent's\n * form catalog carried wrong page counts for 7 of 17 forms). So every slot\n * states the label it believes it is aiming at, and states where that belief\n * came from:\n *\n * - `labelBasis: 'widget'` — the label must match the widget's OWN `/TU`\n * accessibility text inside the PDF. `checkRegistryAgainstBlank` re-checks\n * it against the bytes, so a mis-aimed slot fails a test rather than\n * quietly printing in the wrong box. Every one of Texas Form 205's 64\n * widgets carries `/TU`, so its whole registry can be checked this way.\n * - `labelBasis: 'derived'` — the label came from somewhere else (an XFA\n * template, a published instruction sheet, a human reading the form) and\n * CANNOT be re-checked against the widget. It is evidence for a reviewer,\n * never proof. Measured on IRS f1040: 0 of 199 widgets carry `/TU` or\n * `/Alt`, so its labels can only ever be derived.\n *\n * Declaring the basis is the point. A guess presented as truth is how a second\n * self-confirming artifact gets built on top of the first one.\n */\n\nimport type { PDFDocument, PDFForm } from 'pdf-lib'\n\n/** What kind of widget a slot writes to. */\nexport type FormSlotKind = 'text' | 'checkbox'\n\n/** How a slot's text value is rendered into the box. */\nexport type FormSlotFormat = 'text' | 'currency'\n\n/** Where a slot's `label` came from, and therefore what it can prove. */\nexport type FormLabelBasis = 'widget' | 'derived'\n\n/** One semantic slot: what a caller supplies, and where the platform puts it. */\nexport interface FormSlot {\n /** The name a caller (and a model) uses: `15`, `entity_name`, `agent_is_org`. */\n slot: string\n /**\n * The AcroForm widget path(s) this slot writes.\n *\n * A LIST because one semantic value legitimately occupies several boxes: IRS\n * Form 1040 states adjusted gross income twice, at the foot of page 1 and\n * the head of page 2, so page 2's arithmetic stands alone. Writing only one\n * of them leaves \"subtract line 14 from line 11b\" pointing at an empty box.\n */\n fields: readonly string[]\n kind: FormSlotKind\n /** How the value is rendered. Ignored for `checkbox`. Default `'text'`. */\n format?: FormSlotFormat\n /** What this slot is, in the form's own words. */\n label: string\n /** Whether `label` is checkable against the PDF, or merely recorded. */\n labelBasis: FormLabelBasis\n}\n\n/** A form's complete slot table, pinned to the revision it was derived from. */\nexport interface FormRegistry {\n /** Stable id for the form: `us-irs-1040`, `us-tx-sos-205`. */\n form: string\n /** The agency's own revision marker: `2025`, `Rev. 12-21`. */\n revision: string\n slots: readonly FormSlot[]\n}\n\n/** Why a registry does not match the blank it claims to describe. */\nexport type RegistryProblemCode =\n | 'no_slots'\n | 'no_fields'\n | 'missing_field'\n | 'wrong_kind'\n | 'label_mismatch'\n | 'duplicate_field'\n | 'duplicate_slot'\n\nexport interface RegistryProblem {\n slot: string\n field?: string\n code: RegistryProblemCode\n detail: string\n}\n\nexport interface RegistryCheckResult {\n ok: boolean\n /** Widgets actually compared against the PDF. Zero means nothing was proven. */\n checked: number\n /** Slots whose label was compared against the widget's own `/TU`. */\n labelsChecked: number\n problems: RegistryProblem[]\n}\n\n/** The `/TU` accessibility text a widget carries, if any. */\nexport async function widgetLabel(form: PDFForm, field: string): Promise<string | undefined> {\n const { PDFName, PDFHexString, PDFString } = await import('pdf-lib')\n const target = form.getFieldMaybe(field)\n if (!target) return undefined\n const tooltip = target.acroField.dict.get(PDFName.of('TU'))\n if (tooltip instanceof PDFString || tooltip instanceof PDFHexString) return tooltip.decodeText()\n return undefined\n}\n\n/** Normalize whitespace so a label comparison survives the agency's own typing. */\nfunction normalizeLabel(value: string): string {\n return value.replace(/\\s+/gu, ' ').trim().toLowerCase()\n}\n\n/**\n * Check a registry against the blank it claims to describe.\n *\n * This is the placement check. It runs in a test or in CI, never on the fill\n * path, and it is the ONLY thing that can catch a slot aimed at the wrong box\n * — reading a value back after writing it cannot, because it re-reads the same\n * name it wrote.\n *\n * It fails on ABSENCE, deliberately. tax-agent shipped an audit that silently\n * skipped an expected field the PDF did not expose, so a fill against entirely\n * wrong paths reported `passed=0 failed=0`. Here an empty registry, a slot\n * with no fields, and a field the PDF does not expose are each a problem with\n * a name, and `checked` is reported so a caller can see how much was actually\n * proven rather than trusting a bare `ok: true`.\n */\nexport async function checkRegistryAgainstBlank(args: {\n /** The blank's bytes. Decode a `FormBlank` with `decodeFormBlank` first. */\n pdf: Uint8Array\n registry: FormRegistry\n}): Promise<RegistryCheckResult> {\n const { PDFDocument } = await import('pdf-lib')\n const document: PDFDocument = await PDFDocument.load(args.pdf, { updateMetadata: false })\n const form = document.getForm()\n\n // The PDF's OWN field list, enumerated independently of anything the\n // registry claims. Every lookup below resolves against this, so a registry\n // naming a field that does not exist cannot pass by being skipped.\n const exposed = new Map<string, string>()\n for (const field of form.getFields()) exposed.set(field.getName(), field.constructor.name)\n\n const problems: RegistryProblem[] = []\n const claimedBy = new Map<string, string>()\n const seenSlots = new Set<string>()\n let checked = 0\n let labelsChecked = 0\n\n if (args.registry.slots.length === 0) {\n problems.push({\n slot: '(registry)',\n code: 'no_slots',\n detail: `registry ${args.registry.form} declares no slots — it can prove nothing and fill nothing`,\n })\n }\n\n for (const slot of args.registry.slots) {\n if (seenSlots.has(slot.slot)) {\n problems.push({\n slot: slot.slot,\n code: 'duplicate_slot',\n detail: `slot ${slot.slot} is declared twice; the later entry silently wins at fill time`,\n })\n }\n seenSlots.add(slot.slot)\n\n if (slot.fields.length === 0) {\n problems.push({\n slot: slot.slot,\n code: 'no_fields',\n detail: `slot ${slot.slot} names no widget, so a value supplied for it goes nowhere`,\n })\n continue\n }\n\n for (const field of slot.fields) {\n const previous = claimedBy.get(field)\n if (previous !== undefined && previous !== slot.slot) {\n problems.push({\n slot: slot.slot,\n field,\n code: 'duplicate_field',\n detail: `${field} is claimed by both slot ${previous} and slot ${slot.slot}`,\n })\n }\n claimedBy.set(field, slot.slot)\n\n const actualKind = exposed.get(field)\n if (actualKind === undefined) {\n problems.push({\n slot: slot.slot,\n field,\n code: 'missing_field',\n detail: `${args.registry.form} has no widget named ${field}`,\n })\n continue\n }\n checked += 1\n\n const expectedKind = slot.kind === 'checkbox' ? 'PDFCheckBox' : 'PDFTextField'\n if (actualKind !== expectedKind) {\n problems.push({\n slot: slot.slot,\n field,\n code: 'wrong_kind',\n detail: `${field} is a ${actualKind}, but slot ${slot.slot} declares ${slot.kind}`,\n })\n continue\n }\n\n if (slot.labelBasis !== 'widget') continue\n const onWidget = await widgetLabel(form, field)\n if (onWidget === undefined) {\n problems.push({\n slot: slot.slot,\n field,\n code: 'label_mismatch',\n detail: `slot ${slot.slot} claims labelBasis 'widget' but ${field} carries no /TU text to check it against — the label is derived, not checked`,\n })\n continue\n }\n labelsChecked += 1\n if (normalizeLabel(onWidget) !== normalizeLabel(slot.label)) {\n problems.push({\n slot: slot.slot,\n field,\n code: 'label_mismatch',\n detail: `${field} is labelled ${JSON.stringify(onWidget)} but slot ${slot.slot} claims ${JSON.stringify(slot.label)}`,\n })\n }\n }\n }\n\n return { ok: problems.length === 0, checked, labelsChecked, problems }\n}\n","/**\n * Fill a real agency PDF from a slot → value map.\n *\n * Mechanically this is `pdf-lib` writing an AcroForm: TypeScript, no\n * container, no Python, no network. That matters because it is the only shape\n * that runs everywhere these products run — a Cloudflare Worker (where 100% of\n * tax-agent's production work products were produced, with no sandbox at all)\n * and a sandbox container whose egress proxy refuses the agency's own host.\n *\n * The invariant it enforces is in `registry.ts`: the caller supplies SEMANTIC\n * SLOTS and never a widget name. Two consequences show up here.\n *\n * NOTHING IS SILENT. Every slot ends in `filled` or in `unfilled` with a code\n * and a reason. A value the caller supplied that never reached the page means\n * the document and the data disagree, which is the one thing a reviewer must\n * not have to discover by eye. That includes a registry naming a widget the\n * PDF does not expose: it is reported per-slot rather than crashing the render\n * or — worse — being skipped, which is how tax-agent once audited a fill\n * against entirely wrong paths as `passed=0 failed=0`.\n *\n * NO ON-STATE IS EVER AUTHORED. A checkbox's \"on\" name is a property of the\n * PDF and frequently a hex-escaped sentence: Texas Form 205's seven boxes are\n * `is#20an#20organization`, `initially#20has#20#20managers` (note the double\n * space), and four more like them. A caller that types one of those strings is\n * authoring something it cannot check, so a checkbox slot takes a BOOLEAN and\n * the widget's own on-value is read out of the file. The escaped spelling is\n * not handled — it is unrepresentable.\n */\n\nimport type { PDFCheckBox, PDFDocument, PDFTextField } from 'pdf-lib'\n\nimport type { FormRegistry, FormSlot } from './registry'\n\n/** One widget the fill actually wrote. */\nexport interface FilledWidget {\n slot: string\n field: string\n kind: FormSlot['kind']\n /** The value as the caller supplied it, before formatting. */\n value: unknown\n /** Exactly the text placed in the box. Absent for a checkbox. */\n text?: string\n /** Whether a checkbox was ticked. Absent for a text field. */\n checked?: boolean\n /** The widget's OWN on-state, decoded — read from the PDF, never authored. */\n onState?: string\n}\n\n/** Why a supplied value did not reach the page. */\nexport type UnfilledCode =\n | 'unknown_slot'\n | 'missing_field'\n | 'wrong_kind'\n | 'not_a_number'\n | 'not_a_boolean'\n | 'unformattable'\n\nexport interface UnfilledSlot {\n slot: string\n field?: string\n value: unknown\n code: UnfilledCode\n reason: string\n}\n\nexport interface FillFormResult {\n bytes: Uint8Array\n filled: FilledWidget[]\n unfilled: UnfilledSlot[]\n form: string\n revision: string\n}\n\nexport interface FillFormOptions {\n /** The blank's bytes. Decode a `FormBlank` with `decodeFormBlank` first. */\n pdf: Uint8Array\n registry: FormRegistry\n /** slot name → value. Keys the registry does not know are reported, not dropped. */\n values: Record<string, unknown>\n /**\n * Override how a text value becomes box text. Return `undefined` to fall\n * through to the built-in formatting for the slot's `format`.\n */\n formatText?: (value: unknown, slot: FormSlot) => string | undefined\n}\n\n/**\n * Format a figure the way a US agency form prints it: grouped thousands, two\n * decimals, negatives in parentheses.\n *\n * Two decimals rather than whole dollars because the caller's data carries\n * cents and a reviewer compares the document against that data — rounding here\n * would manufacture a disagreement between the two on every line with cents.\n */\nexport function formatFormCurrency(value: number): string {\n const magnitude = Math.abs(value).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n })\n return value < 0 ? `(${magnitude})` : magnitude\n}\n\n/** A number, or a number written the way a form prints one (`$141,318.74`). */\nexport function parseFormAmount(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value !== 'string') return undefined\n const normalized = value.replace(/[$,\\s]/gu, '')\n if (!/^-?\\d+(?:\\.\\d+)?$/u.test(normalized)) return undefined\n const parsed = Number(normalized)\n return Number.isFinite(parsed) ? parsed : undefined\n}\n\n/**\n * A checkbox takes a boolean and nothing else.\n *\n * Strict on purpose. Accepting a truthy string would accept the widget's own\n * escaped on-state (`'is#20an#20organization'`) as \"true\", which is exactly\n * the authored-on-state this module refuses — and it would accept `'no'` as\n * true, silently ticking a box the caller meant to leave clear.\n */\nexport function parseFormBoolean(value: unknown): boolean | undefined {\n if (typeof value === 'boolean') return value\n if (typeof value !== 'string') return undefined\n const normalized = value.trim().toLowerCase()\n if (normalized === 'true' || normalized === 'yes') return true\n if (normalized === 'false' || normalized === 'no') return false\n return undefined\n}\n\nfunction textFor(\n value: unknown,\n slot: FormSlot,\n override: FillFormOptions['formatText'],\n): { text: string } | { code: UnfilledCode; reason: string } {\n const custom = override?.(value, slot)\n if (custom !== undefined) return { text: custom }\n if ((slot.format ?? 'text') === 'currency') {\n const amount = parseFormAmount(value)\n if (amount === undefined) return { code: 'not_a_number', reason: 'not a numeric amount' }\n return { text: formatFormCurrency(amount) }\n }\n if (typeof value === 'string') return { text: value }\n if (typeof value === 'number' && Number.isFinite(value)) return { text: String(value) }\n return { code: 'unformattable', reason: `cannot render a ${typeof value} into a text box` }\n}\n\n/**\n * Write a value map onto a blank, returning the bytes plus the exact\n * widget-by-widget account of what was and was not placed.\n */\nexport async function fillPdfForm(options: FillFormOptions): Promise<FillFormResult> {\n const { PDFCheckBox: CheckBox, PDFDocument, PDFName, PDFTextField: TextField } = await import('pdf-lib')\n\n // `updateMetadata: false` keeps the render deterministic — pdf-lib otherwise\n // stamps a ModDate, which would give the same values a different checksum on\n // every call and defeat content-addressed storage. It also keeps our\n // timestamps off the agency's document.\n const document: PDFDocument = await PDFDocument.load(options.pdf, { updateMetadata: false })\n const form = document.getForm()\n\n // Agency forms ship as AcroForm/XFA hybrids, and a reader that prefers the\n // XFA layer would draw the ORIGINAL empty form and show none of these\n // values. `getForm()` drops the XFA packet (pdf-lib does not support it), so\n // this asserts the property rather than trusting it to stay incidental.\n if (document.catalog.getOrCreateAcroForm().dict.has(PDFName.of('XFA'))) {\n throw new Error('filled form still carries an XFA layer — a viewer would draw the blank instead')\n }\n\n const bySlot = new Map(options.registry.slots.map((slot) => [slot.slot, slot]))\n const filled: FilledWidget[] = []\n const unfilled: UnfilledSlot[] = []\n\n for (const [name, value] of Object.entries(options.values)) {\n const slot = bySlot.get(name)\n if (!slot) {\n unfilled.push({\n slot: name,\n value,\n code: 'unknown_slot',\n reason: `${options.registry.form} has no slot named ${name}`,\n })\n continue\n }\n\n if (slot.kind === 'checkbox') {\n const checked = parseFormBoolean(value)\n if (checked === undefined) {\n unfilled.push({\n slot: name,\n value,\n code: 'not_a_boolean',\n reason: 'a checkbox takes true or false; a checkbox on-state is read from the PDF and is never supplied',\n })\n continue\n }\n for (const field of slot.fields) {\n const widget = form.getFieldMaybe(field)\n if (!widget) {\n unfilled.push({ slot: name, field, value, code: 'missing_field', reason: `${options.registry.form} has no widget named ${field}` })\n continue\n }\n if (!(widget instanceof CheckBox)) {\n unfilled.push({ slot: name, field, value, code: 'wrong_kind', reason: `${field} is a ${widget.constructor.name}, not a checkbox` })\n continue\n }\n const box = widget as PDFCheckBox\n // The widget's OWN on-state. Read, never authored: Texas Form 205's\n // are hex-escaped sentences, and a caller that typed one would be\n // asserting a fact about the file it cannot check.\n const onState = box.acroField.getOnValue()?.decodeText()\n if (checked) box.check()\n else box.uncheck()\n filled.push({ slot: name, field, kind: 'checkbox', value, checked, onState })\n }\n continue\n }\n\n const rendered = textFor(value, slot, options.formatText)\n if ('code' in rendered) {\n unfilled.push({ slot: name, value, code: rendered.code, reason: rendered.reason })\n continue\n }\n for (const field of slot.fields) {\n const widget = form.getFieldMaybe(field)\n if (!widget) {\n unfilled.push({ slot: name, field, value, code: 'missing_field', reason: `${options.registry.form} has no widget named ${field}` })\n continue\n }\n if (!(widget instanceof TextField)) {\n unfilled.push({ slot: name, field, value, code: 'wrong_kind', reason: `${field} is a ${widget.constructor.name}, not a text field` })\n continue\n }\n ;(widget as PDFTextField).setText(rendered.text)\n filled.push({ slot: name, field, kind: 'text', value, text: rendered.text })\n }\n }\n\n return {\n bytes: await document.save(),\n filled,\n unfilled,\n form: options.registry.form,\n revision: options.registry.revision,\n }\n}\n\n/**\n * What to tell the agent after a fill.\n *\n * Names the values that did NOT reach the page. Those are exactly the cases\n * where the document and the data disagree, and the agent is the only party\n * that can resolve it — returning a bare \"done\" hands a reviewer a form\n * missing values the data claims are on it.\n */\nexport function describeFormFill(result: FillFormResult): string {\n const count = result.filled.length\n const base = `Filled ${result.form} (${result.revision}) with ${count} ${count === 1 ? 'value' : 'values'}.`\n if (result.unfilled.length === 0) return base\n const detail = result.unfilled.map((entry) => `${entry.slot} (${entry.reason})`).join('; ')\n return `${base} NOT placed on the form: ${detail}. Those values are in the data but not on the document a reviewer opens.`\n}\n","/**\n * Read a filled form back and say, slot by slot, whether the document agrees\n * with the data it was built from.\n *\n * WHAT THIS PROVES, AND WHAT IT CANNOT\n *\n * This proves the WRITE LANDED: the value reached a real widget, that widget\n * exists in the produced file, and it holds the text or tick the data claims.\n * It does NOT prove PLACEMENT — that the widget is the right box on the page —\n * because it resolves the same field names the fill used. Reading a value back\n * by the name you just wrote passes even when the name was invented; measured\n * on tax-agent, an audit of that shape reported 3 passed / 0 failed for three\n * figures sitting in \"Combat zone\" and the date boxes.\n *\n * Placement is proven by `checkRegistryAgainstBlank`, which compares each\n * slot's claimed label against the widget's own `/TU` text inside the PDF.\n * The two checks are complements, and a product that runs only this one has\n * the audit that already failed once. That is stated here rather than in a\n * commit message because a future caller will otherwise reach for the\n * convenient half.\n *\n * FAILS ON ABSENCE. A registry field the produced PDF does not expose is\n * `missing_field`, and an empty `expected` map is `ok: false` — the\n * `passed=0 failed=0` verdict is the exact shape of the bug this replaces.\n */\n\nimport type { PDFCheckBox, PDFDocument, PDFTextField } from 'pdf-lib'\n\nimport { parseFormBoolean, formatFormCurrency, parseFormAmount } from './fill'\nimport { widgetLabel, type FormRegistry } from './registry'\n\nexport type SlotVerdict = 'ok' | 'unknown_slot' | 'missing_field' | 'wrong_kind' | 'not_written' | 'mismatch'\n\n/** What the slot's `label` is worth, checked against the produced file. */\nexport type LabelVerdict = 'matches_widget' | 'label_mismatch' | 'derived_unchecked' | 'no_widget_label'\n\nexport interface SlotVerification {\n slot: string\n field?: string\n verdict: SlotVerdict\n /** What the data says the box should hold. */\n expected?: string\n /** What the box actually holds, read out of the produced bytes. */\n actual?: string\n labelVerdict?: LabelVerdict\n label?: string\n}\n\nexport interface VerifyFormResult {\n ok: boolean\n /** Widgets actually read back. Zero means nothing was proven. */\n verified: number\n slots: SlotVerification[]\n}\n\nfunction expectedText(value: unknown, format: string | undefined): string {\n if (format === 'currency') {\n const amount = parseFormAmount(value)\n return amount === undefined ? String(value) : formatFormCurrency(amount)\n }\n return typeof value === 'number' ? String(value) : String(value)\n}\n\n/**\n * Verify a filled form against the value map it was filled from.\n *\n * `expected` is the SAME shape passed to `fillPdfForm` — deliberately, so a\n * caller cannot verify against a convenient restatement of what it wrote.\n */\nexport async function verifyFilledForm(args: {\n /** The PRODUCED bytes, not the blank. */\n pdf: Uint8Array\n registry: FormRegistry\n expected: Record<string, unknown>\n /** Slots the caller knowingly left out of `expected`; anything else is checked. */\n}): Promise<VerifyFormResult> {\n const { PDFCheckBox: CheckBox, PDFDocument, PDFTextField: TextField } = await import('pdf-lib')\n const document: PDFDocument = await PDFDocument.load(args.pdf, { updateMetadata: false })\n const form = document.getForm()\n\n // The produced file's OWN field list, enumerated before anything the\n // registry claims is consulted. A registry field absent from this set is a\n // failure, never a skip.\n const exposed = new Set(form.getFields().map((field) => field.getName()))\n\n const bySlot = new Map(args.registry.slots.map((slot) => [slot.slot, slot]))\n const slots: SlotVerification[] = []\n let verified = 0\n let ok = true\n\n for (const [name, value] of Object.entries(args.expected)) {\n const slot = bySlot.get(name)\n if (!slot) {\n slots.push({ slot: name, verdict: 'unknown_slot' })\n ok = false\n continue\n }\n for (const field of slot.fields) {\n if (!exposed.has(field)) {\n slots.push({ slot: name, field, verdict: 'missing_field' })\n ok = false\n continue\n }\n const widget = form.getField(field)\n const label = await widgetLabel(form, field)\n const labelVerdict: LabelVerdict =\n slot.labelBasis === 'derived'\n ? 'derived_unchecked'\n : label === undefined\n ? 'no_widget_label'\n : label.replace(/\\s+/gu, ' ').trim().toLowerCase() ===\n slot.label.replace(/\\s+/gu, ' ').trim().toLowerCase()\n ? 'matches_widget'\n : 'label_mismatch'\n if (labelVerdict === 'label_mismatch' || labelVerdict === 'no_widget_label') ok = false\n\n if (slot.kind === 'checkbox') {\n if (!(widget instanceof CheckBox)) {\n slots.push({ slot: name, field, verdict: 'wrong_kind', label, labelVerdict })\n ok = false\n continue\n }\n const want = parseFormBoolean(value)\n const got = (widget as PDFCheckBox).isChecked()\n const verdict: SlotVerdict = want === undefined ? 'mismatch' : got === want ? 'ok' : 'mismatch'\n if (verdict !== 'ok') ok = false\n else verified += 1\n slots.push({\n slot: name,\n field,\n verdict,\n expected: String(want),\n actual: String(got),\n label,\n labelVerdict,\n })\n continue\n }\n\n if (!(widget instanceof TextField)) {\n slots.push({ slot: name, field, verdict: 'wrong_kind', label, labelVerdict })\n ok = false\n continue\n }\n const want = expectedText(value, slot.format)\n const got = (widget as PDFTextField).getText() ?? ''\n const verdict: SlotVerdict = got === '' && want !== '' ? 'not_written' : got === want ? 'ok' : 'mismatch'\n if (verdict !== 'ok') ok = false\n else verified += 1\n slots.push({ slot: name, field, verdict, expected: want, actual: got, label, labelVerdict })\n }\n }\n\n // An audit that checked nothing must never report success. This is the\n // `passed=0 failed=0` verdict, refused by name.\n if (verified === 0) ok = false\n\n return { ok, verified, slots }\n}\n"],"mappings":"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","names":["want","got","verdict"]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InviteAcceptPage
|
|
3
|
+
} from "../chunk-VCPZ3HTN.js";
|
|
1
4
|
import {
|
|
2
5
|
MembersPanel
|
|
3
6
|
} from "../chunk-S564OFTL.js";
|
|
4
7
|
import {
|
|
5
8
|
InvitationsPanel
|
|
6
9
|
} from "../chunk-5SXS3YAB.js";
|
|
7
|
-
import {
|
|
8
|
-
InviteAcceptPage
|
|
9
|
-
} from "../chunk-VCPZ3HTN.js";
|
|
10
10
|
import "../chunk-6XIAPIW6.js";
|
|
11
11
|
export {
|
|
12
12
|
InvitationsPanel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.22",
|
|
4
4
|
"packageManager": "pnpm@10.33.4",
|
|
5
5
|
"description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
|
|
6
6
|
"keywords": [
|
|
@@ -147,6 +147,11 @@
|
|
|
147
147
|
"import": "./dist/preflight/index.js",
|
|
148
148
|
"default": "./dist/preflight/index.js"
|
|
149
149
|
},
|
|
150
|
+
"./forms": {
|
|
151
|
+
"types": "./dist/forms/index.d.ts",
|
|
152
|
+
"import": "./dist/forms/index.js",
|
|
153
|
+
"default": "./dist/forms/index.js"
|
|
154
|
+
},
|
|
150
155
|
"./object-store": {
|
|
151
156
|
"types": "./dist/object-store/index.d.ts",
|
|
152
157
|
"import": "./dist/object-store/index.js",
|
|
@@ -444,6 +449,7 @@
|
|
|
444
449
|
"knip": "^5.46.0",
|
|
445
450
|
"konva": "^10.3.0",
|
|
446
451
|
"lucide-react": "^1.16.0",
|
|
452
|
+
"pdf-lib": "^1.17.1",
|
|
447
453
|
"react": "^19.0.0",
|
|
448
454
|
"react-dom": "^19.2.7",
|
|
449
455
|
"react-konva": "^19.2.5",
|
|
@@ -456,15 +462,15 @@
|
|
|
456
462
|
"peerDependencies": {
|
|
457
463
|
"@huggingface/transformers": ">=3",
|
|
458
464
|
"@radix-ui/react-dialog": ">=1.1",
|
|
459
|
-
"@tangle-network/agent-eval": "0.133.2",
|
|
465
|
+
"@tangle-network/agent-eval": ">=0.133.2",
|
|
460
466
|
"@tangle-network/agent-integrations": ">=0.44.0",
|
|
461
|
-
"@tangle-network/agent-interface": "0.36.0",
|
|
462
|
-
"@tangle-network/agent-knowledge": "6.1.4",
|
|
463
|
-
"@tangle-network/agent-profile-materialize": "0.9.0",
|
|
464
|
-
"@tangle-network/agent-runtime": "0.107.4",
|
|
465
|
-
"@tangle-network/brand": "1.1.0",
|
|
466
|
-
"@tangle-network/sandbox": "0.15.1",
|
|
467
|
-
"@tangle-network/sandbox-ui": "0.90.1",
|
|
467
|
+
"@tangle-network/agent-interface": ">=0.36.0",
|
|
468
|
+
"@tangle-network/agent-knowledge": ">=6.1.4",
|
|
469
|
+
"@tangle-network/agent-profile-materialize": ">=0.9.0",
|
|
470
|
+
"@tangle-network/agent-runtime": ">=0.107.4",
|
|
471
|
+
"@tangle-network/brand": ">=1.1.0",
|
|
472
|
+
"@tangle-network/sandbox": ">=0.15.1",
|
|
473
|
+
"@tangle-network/sandbox-ui": ">=0.90.1",
|
|
468
474
|
"@xyflow/react": ">=12.0.0",
|
|
469
475
|
"better-auth": ">=1.6.16",
|
|
470
476
|
"drizzle-orm": ">=0.36",
|
|
@@ -473,7 +479,8 @@
|
|
|
473
479
|
"react": ">=18",
|
|
474
480
|
"react-konva": ">=18",
|
|
475
481
|
"react-router": ">=7",
|
|
476
|
-
"resend": ">=6"
|
|
482
|
+
"resend": ">=6",
|
|
483
|
+
"pdf-lib": ">=1.17"
|
|
477
484
|
},
|
|
478
485
|
"peerDependenciesMeta": {
|
|
479
486
|
"@huggingface/transformers": {
|
|
@@ -526,6 +533,9 @@
|
|
|
526
533
|
},
|
|
527
534
|
"resend": {
|
|
528
535
|
"optional": true
|
|
536
|
+
},
|
|
537
|
+
"pdf-lib": {
|
|
538
|
+
"optional": true
|
|
529
539
|
}
|
|
530
540
|
},
|
|
531
541
|
"dependencies": {
|