@tinkerlandai/indiana-iam 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -7
- package/assets/viewer.js +55 -2
- package/package.json +3 -1
- package/src/analyze.mjs +168 -90
- package/src/anchors.mjs +127 -0
- package/src/build-options.d.ts +7 -0
- package/src/build.mjs +13 -19
- package/src/cli.mjs +10 -4
- package/src/dom-payload-browser.mjs +28 -0
- package/src/dom-payload-json.mjs +12 -0
- package/src/dom-payload.mjs +50 -0
- package/src/framework-routes.mjs +41 -88
- package/src/generate.mjs +18 -6
- package/src/git-metadata.mjs +61 -0
- package/src/index.d.ts +114 -12
- package/src/index.mjs +3 -1
- package/src/instrument.mjs +2 -2
- package/src/integrity.mjs +4 -2
- package/src/library-entry.mjs +2 -0
- package/src/next-loader.cjs +30 -0
- package/src/next.mjs +26 -3
- package/src/node-hashes.mjs +5 -2
- package/src/rebind.mjs +116 -0
- package/src/release-metadata.mjs +1 -1
- package/src/render-index.mjs +32 -11
- package/src/route-tree.mjs +5 -12
- package/src/routing.mjs +1 -1
- package/src/schema-definition.mjs +8 -1
- package/src/source-entry.mjs +50 -0
- package/src/source-files.mjs +53 -0
- package/src/source-resolution.mjs +268 -0
- package/src/tanstack-routes.mjs +143 -0
- package/src/transform.mjs +32 -0
- package/src/upload.mjs +30 -16
- package/src/validate.mjs +14 -4
- package/src/vite.d.ts +15 -2
- package/src/vite.mjs +33 -7
package/README.md
CHANGED
|
@@ -40,7 +40,14 @@ Point the visual SDK at the descriptor:
|
|
|
40
40
|
|
|
41
41
|
`next dev` and other phases leave your config unchanged. Add `public/.well-known/indiana/` to `.gitignore`.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
No developer harness or business SDK npm dependency is needed. The public script above runs the visual runtime;
|
|
44
|
+
Indiana owns authorization, immutable uploads and experiment publication.
|
|
45
|
+
IAM does not discover or launch merchant harnesses, test scripts, agents, browsers or development servers. It does
|
|
46
|
+
not import merchant modules to execute them. Source enumeration and optional Git commit labels use filesystem reads;
|
|
47
|
+
the package starts no external commands and works without a Git executable. Developers' explicitly configured
|
|
48
|
+
framework callbacks remain part of their normal build.
|
|
49
|
+
|
|
50
|
+
## Vite, React Router and TanStack Router
|
|
44
51
|
|
|
45
52
|
```js
|
|
46
53
|
// vite.config.mjs
|
|
@@ -49,14 +56,41 @@ import { indianaIam } from "@tinkerlandai/indiana-iam/vite";
|
|
|
49
56
|
export default { plugins: [indianaIam()] };
|
|
50
57
|
```
|
|
51
58
|
|
|
59
|
+
Use an ESM Vite config (`vite.config.mjs` or `vite.config.mts`, or a project with `"type": "module"`).
|
|
60
|
+
CommonJS configs cannot `require` this ESM package.
|
|
61
|
+
|
|
52
62
|
Client builds expose `import.meta.env.INDIANA_IAM_RELEASE_URL` and write the descriptor into the build output folder.
|
|
53
63
|
Server builds are skipped.
|
|
54
64
|
|
|
65
|
+
Browser apps use the single local module script in `index.html` to select reachable router and React root declarations.
|
|
66
|
+
For multiple entry points, set `indianaIam({ entry: "src/main.tsx" })`, or pass `--entry src/main.tsx` to standalone generation.
|
|
67
|
+
The path must stay inside the scanned root. Next.js uses its App/Pages root instead.
|
|
68
|
+
|
|
55
69
|
## Options
|
|
56
70
|
|
|
57
|
-
Both integrations accept `{ enabled, name, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload }`;
|
|
71
|
+
Both integrations accept `{ enabled, name, automaticAnchors, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload, client }`;
|
|
72
|
+
browser applications additionally accept `entry`;
|
|
58
73
|
`withIndianaIam` also takes `root`. By default a source the scanner cannot read prints a warning and the build continues without an IAM;
|
|
59
74
|
`strict: true` fails the build instead. The same source always produces the same release ID and IAM bytes.
|
|
75
|
+
If source changes during compilation, generation fails instead of publishing a descriptor for different code.
|
|
76
|
+
|
|
77
|
+
## Target identity and release changes
|
|
78
|
+
|
|
79
|
+
Framework builds reuse `id`, `data-testid`, `data-test`, `data-qa`, then `data-indiana-id`. When none is declared,
|
|
80
|
+
the source loader adds a deterministic `data-indiana-id` to unambiguous native JSX in the build output and IAM.
|
|
81
|
+
Source files stay unchanged; formatting, comments and unrelated sibling insertions preserve a leaf's marker.
|
|
82
|
+
Set `automaticAnchors: false` to use only existing attributes.
|
|
83
|
+
|
|
84
|
+
Repeated component instances, identical source sites, unknown props and duplicate DOM selectors cannot prove a
|
|
85
|
+
unique target and remain read-only. React `key` contributes source evidence but is not a DOM selector or a global ID;
|
|
86
|
+
dynamic keys are never evaluated a second time or exposed as customer identifiers. Supported routing and state
|
|
87
|
+
coverage stays bounded and explicit.
|
|
88
|
+
|
|
89
|
+
The visual runtime requests delivery for the page's exact IAM digest. Indiana rebinds saved changes only when the
|
|
90
|
+
route, composition, target selector, element content and render uniqueness still match. Text changes also require
|
|
91
|
+
unchanged subtree content. A failure excludes the whole experiment for that release; it does not renormalize
|
|
92
|
+
variation weights or rewrite authored pins. Old deployments keep their own bindings during a rolling release.
|
|
93
|
+
The Console reports incompatible references for review. Local extraction does not certify arbitrary runtime data.
|
|
60
94
|
|
|
61
95
|
## Other builds
|
|
62
96
|
|
|
@@ -67,12 +101,26 @@ indiana-iam prepare --root .
|
|
|
67
101
|
indiana-iam anchor --root . --out <plan.json> [--apply]
|
|
68
102
|
```
|
|
69
103
|
|
|
70
|
-
|
|
71
|
-
|
|
104
|
+
Standalone generation uses existing attributes. Custom builds can pair `transformSourceForIam(code, relativeFile)`
|
|
105
|
+
with `generateAppManifest({ root, automaticAnchors: true })`; use both on the same original source. `anchor` is an
|
|
106
|
+
optional command for explicit source annotations. Next and Vite do not need that command or a harness.
|
|
72
107
|
|
|
73
108
|
## Scope
|
|
74
109
|
|
|
75
|
-
Supported source: Next.js App Router and Pages Router,
|
|
110
|
+
Supported source: Next.js App Router and Pages Router, React Router / single-root React apps built with Vite,
|
|
111
|
+
and statically generated TanStack file-route trees registered with `createRouter({ routeTree })`.
|
|
112
|
+
React Router supports imported static path constants, inline `Component` functions and source-proven lazy import forwarding.
|
|
113
|
+
TanStack reads the registered generated tree, including pathless layouts, index routes and simple `$param` paths;
|
|
114
|
+
custom history/basepath and complex parameter patterns require separate adapters. Loaders, guards, retry/reload behavior
|
|
115
|
+
and runtime data remain unobserved. Unsupported optional React paths are isolated as gaps.
|
|
116
|
+
The complete validated IAM must fit within 8 MiB; oversized projections fail without emitting a truncated artifact.
|
|
117
|
+
Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
|
|
118
|
+
and `nodes: []` in that document's wire record. This lossless storage encoding preserves all source IDs, DOM values,
|
|
119
|
+
HTML/DOM hashes and targets. The document hash pins its encoded storage body; decoding is local and bounded to
|
|
120
|
+
8 MiB per payload and 32 MiB in aggregate. The final IAM file limit stays 8 MiB. Exported page HTML is expanded.
|
|
121
|
+
Use the matching 0.3 generator, validator and viewer together. Indiana ingestion/viewer sources must be updated
|
|
122
|
+
before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
|
|
123
|
+
viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
|
|
76
124
|
The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
|
|
77
125
|
step calls Indiana, and only when a key is set.
|
|
78
126
|
Values that look like credentials, tokens, emails or private URLs are removed, but review what your pages contain.
|
|
@@ -81,13 +129,61 @@ Values that look like credentials, tokens, emails or private URLs are removed, b
|
|
|
81
129
|
|
|
82
130
|
Create a Key with only the `app-manifests:write` permission (Console → developer settings) and set it as
|
|
83
131
|
`INDIANA_IAM_KEY` in your build environment. Each production build then registers the IAM, stores it through a
|
|
84
|
-
server-signed URL and asks Indiana to validate it; the validated version becomes
|
|
132
|
+
server-signed URL and asks Indiana to validate it; the validated version becomes its client's current version.
|
|
85
133
|
Rebuilding unchanged source only makes the existing version current again.
|
|
86
134
|
|
|
135
|
+
An App environment can hold several clients, each a named build with its own current version: for example `web` and
|
|
136
|
+
`web-dashboard`, or later `ios` and `android` apps. Set the `client` option (or `INDIANA_IAM_CLIENT`) to 1-40 lowercase
|
|
137
|
+
letters, digits or hyphens starting with a letter; the default is `web`. A client keeps the platform of its first
|
|
138
|
+
upload, and the same IAM bytes cannot be uploaded for two clients.
|
|
139
|
+
|
|
87
140
|
- Uploads go to `https://tinker.cash`; set `INDIANA_IAM_BASE_URL` (or the `baseUrl` option) for another Indiana
|
|
88
141
|
environment.
|
|
89
142
|
- On Vercel only production deployments upload by default; `upload: "always"` uploads every build and
|
|
90
143
|
`upload: false` never uploads.
|
|
91
144
|
- A failed upload prints a warning and the build continues, unless `strict: true`.
|
|
92
145
|
- The App's Owner or Admin must turn on the App page map first.
|
|
93
|
-
- `INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest
|
|
146
|
+
- `INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest> [--client <key>]` uploads a file generated elsewhere.
|
|
147
|
+
|
|
148
|
+
## App map annotations
|
|
149
|
+
|
|
150
|
+
The Console App map groups pages into areas, shows recorded navigation, and opens a main-path workbench before
|
|
151
|
+
opening a page editor. Thumbnails are static source projections. They do not execute authentication, data loaders,
|
|
152
|
+
form submissions or application code.
|
|
153
|
+
|
|
154
|
+
Optional static attributes preserve product intent that cannot be established from a URL alone:
|
|
155
|
+
|
|
156
|
+
| Attribute | Location | Meaning |
|
|
157
|
+
| -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `data-indiana-map-area` | Page root | Functional area label; otherwise the first URL segment groups the directory. |
|
|
159
|
+
| `data-indiana-map-flow` | Page root | Display name for the process containing this page. |
|
|
160
|
+
| `data-indiana-map-kind="login"` | Page root | Declares an authentication page. |
|
|
161
|
+
| `data-indiana-map-auth="public\|required"` | Page root | Declares the page's access requirement. Omission remains unknown. |
|
|
162
|
+
| `data-indiana-map-edge="step\|tab\|login\|return"` | An existing link or supported UI control | Classifies an existing recorded action. It never invents a destination or makes an unsupported action executable. |
|
|
163
|
+
|
|
164
|
+
Use one consistent declaration on the outer page element. Native `role="tab"` also classifies a Tab control.
|
|
165
|
+
A link Tab retains its exact destination URL, including query and fragment; a supported state-writing Tab remains
|
|
166
|
+
an in-page action. Login and return links remain separate from ordinary steps. Unsupported actions appear as
|
|
167
|
+
unknown outcomes. Authentication requirements are display declarations, not proof of a session or runtime access.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<main
|
|
171
|
+
data-indiana-map-area="Workspace"
|
|
172
|
+
data-indiana-map-flow="Create project"
|
|
173
|
+
data-indiana-map-auth="required"
|
|
174
|
+
>
|
|
175
|
+
<a href="/login?next=%2Fprojects" data-indiana-map-edge="login">
|
|
176
|
+
Sign in
|
|
177
|
+
</a>
|
|
178
|
+
<a href="/projects/configure" data-indiana-map-edge="step">
|
|
179
|
+
Continue
|
|
180
|
+
</a>
|
|
181
|
+
<a href="/projects?tab=activity" role="tab">
|
|
182
|
+
Activity
|
|
183
|
+
</a>
|
|
184
|
+
</main>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
An unambiguous source path can continue automatically until a branch or cycle. Explicit step links take precedence
|
|
188
|
+
over ordinary navigation; login/return links and Tabs are inspected separately. A new IAM build is needed to add
|
|
189
|
+
these annotations to an already uploaded version. Existing immutable versions are never rewritten.
|
package/assets/viewer.js
CHANGED
|
@@ -489,6 +489,44 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
489
489
|
}
|
|
490
490
|
// Verify the compact source document first, then derive its exact v1 hashes.
|
|
491
491
|
// Work is page-local and cancellable; hashes never become another saved authority.
|
|
492
|
+
/* BEGIN COMPRESSED DOM */
|
|
493
|
+
function checkedDomPayload(document) {
|
|
494
|
+
const payload = document.domPayload;
|
|
495
|
+
if (!payload) return null;
|
|
496
|
+
if (document.html !== "" || !Array.isArray(document.nodes) || document.nodes.length || Object.keys(payload).sort().join(",") !== "bytes,data,encoding" || payload.encoding !== "gzip-json-v1" || !Number.isSafeInteger(payload.bytes) || payload.bytes < 1 || payload.bytes > 8_388_608 || typeof payload.data !== "string" || !payload.data.length || payload.data.length > 11_184_812 || payload.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(payload.data)) throw Error("Invalid compressed DOM payload");
|
|
497
|
+
return payload;
|
|
498
|
+
}
|
|
499
|
+
function parsedDomPayload(text) {
|
|
500
|
+
const value = JSON.parse(text);
|
|
501
|
+
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" || value.html.length > 2_000_000 || !value.html.isWellFormed() || !Array.isArray(value.nodes) || value.nodes.length > 12000 || JSON.stringify(value) !== text) throw Error("Invalid expanded DOM payload");
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
async function documentDomBrowser(document) {
|
|
505
|
+
const payload = checkedDomPayload(document);
|
|
506
|
+
if (!payload) return { html: document.html, nodes: document.nodes };
|
|
507
|
+
if (typeof DecompressionStream !== "function") throw Error("Compressed IAM requires DecompressionStream support");
|
|
508
|
+
const binary = atob(payload.data);
|
|
509
|
+
if (btoa(binary) !== payload.data) throw Error("Noncanonical compressed DOM base64");
|
|
510
|
+
const compressed = Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
511
|
+
const stream = new ReadableStream({ start(controller) { controller.enqueue(compressed); controller.close(); } }).pipeThrough(new DecompressionStream("gzip"));
|
|
512
|
+
const reader = stream.getReader(), chunks = [];
|
|
513
|
+
let total = 0;
|
|
514
|
+
try {
|
|
515
|
+
for (;;) {
|
|
516
|
+
const next = await reader.read();
|
|
517
|
+
if (next.done) break;
|
|
518
|
+
total += next.value.byteLength;
|
|
519
|
+
if (total > payload.bytes) throw Error("Expanded DOM byte budget exceeded");
|
|
520
|
+
chunks.push(next.value);
|
|
521
|
+
}
|
|
522
|
+
} finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
523
|
+
if (total !== payload.bytes) throw Error("Expanded DOM byte length mismatch");
|
|
524
|
+
const decoded = new Uint8Array(total);
|
|
525
|
+
let offset = 0;
|
|
526
|
+
for (const chunk of chunks) { decoded.set(chunk, offset); offset += chunk.byteLength; }
|
|
527
|
+
return parsedDomPayload(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
|
|
528
|
+
}
|
|
529
|
+
/* END COMPRESSED DOM */
|
|
492
530
|
/* BEGIN HTML DOM DERIVATION */
|
|
493
531
|
function expandDomNodes(document, body) {
|
|
494
532
|
if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash||document.nodes.length>12000)throw Error('Invalid HTML-derived DOM mode');
|
|
@@ -507,6 +545,14 @@ function expandDomNodes(document, body) {
|
|
|
507
545
|
}
|
|
508
546
|
/* END HTML DOM DERIVATION */
|
|
509
547
|
async function expandNodeHashes(document,request) {
|
|
548
|
+
if (document.domPayload) {
|
|
549
|
+
const {hashes, ...body} = document;
|
|
550
|
+
if (await hash("iam/document/v1\0" + canonical(body)) !== hashes.document) throw Error("Compressed document hash mismatch");
|
|
551
|
+
const decoded = await documentDomBrowser(document);
|
|
552
|
+
if (await hash(decoded.html) !== hashes.html || await hash("iam/dom-map/v1\0" + canonical({rootNodeId: document.rootNodeId, nodes: decoded.nodes, sourceMappingsHash: document.sourceMappingsHash})) !== hashes.dom) throw Error("Expanded DOM hash mismatch");
|
|
553
|
+
const {domPayload, ...plain} = document;
|
|
554
|
+
document = {...plain, ...decoded};
|
|
555
|
+
}
|
|
510
556
|
if(!Array.isArray(document.nodes)||document.nodes.length>12000)throw Error('Node hash budget');
|
|
511
557
|
const nodes=expandDomNodes(document,new DOMParser().parseFromString('<!doctype html><html><body>'+document.html+'</body></html>','text/html').body),byId=new Map(nodes.map(node=>[node.nodeId,node]));
|
|
512
558
|
if(byId.size!==nodes.length)throw Error('Duplicate node');
|
|
@@ -601,7 +647,8 @@ function expandDomNodes(document, body) {
|
|
|
601
647
|
if (data.type === "open-page" && typeof data.routeKey === "string") {
|
|
602
648
|
const next = m.pages.find(p => p.pageKey === data.routeKey);
|
|
603
649
|
if (!next) return;
|
|
604
|
-
|
|
650
|
+
if(typeof data.renderKey==="string"&&!m.routeTree.renders.some(r=>r.routeKey===next.pageKey&&r.renderKey===data.renderKey))return;
|
|
651
|
+
const variableRender=typeof data.renderKey==="string"?data.renderKey:m.routeTree.variables.find(variable=>variable.variableKey===data.variableKey)?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===data.variableKey)?.renderKey;
|
|
605
652
|
const targetUrl=typeof data.url==="string"?data.url:(next.pageKey===page.pageKey?currentUrl:next.route);
|
|
606
653
|
if (!doc || next.pageKey !== page.pageKey || variableRender && variableRender!==render?.renderKey || targetUrl!==currentUrl) openPage(next,targetUrl,variableRender);
|
|
607
654
|
else consoleContext?.resume?.();
|
|
@@ -801,8 +848,14 @@ function expandDomNodes(document, body) {
|
|
|
801
848
|
if (m.documents.length) {
|
|
802
849
|
const initialRequest = {};
|
|
803
850
|
pendingDocument = initialRequest;
|
|
804
|
-
|
|
851
|
+
const expandedBytes = m.documents.reduce((total, document) => total + (document.domPayload?.bytes ?? new TextEncoder().encode(JSON.stringify({html:document.html,nodes:document.nodes})).byteLength), 0);
|
|
852
|
+
if (!Number.isSafeInteger(expandedBytes) || expandedBytes > 33_554_432) throw Error("Expanded DOM aggregate byte budget exceeded");
|
|
853
|
+
// Sequential decoding bounds peak memory for a multi-route artifact.
|
|
854
|
+
const expandedDocuments = [];
|
|
855
|
+
for (const document of m.documents) expandedDocuments.push(await expandNodeHashes(document, initialRequest));
|
|
856
|
+
m.documents = expandedDocuments;
|
|
805
857
|
pendingDocument = null;
|
|
858
|
+
renderNav();
|
|
806
859
|
}
|
|
807
860
|
openPage(page,consoleContext?.initialPage?.url??page.route,consoleContext?.initialPage?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===consoleContext.initialPage?.variableKey)?.renderKey);
|
|
808
861
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinkerlandai/indiana-iam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Build-time Indiana App Manifest (IAM) generator for Next.js and Vite apps: scans source after the production build and writes the IAM and its release descriptor.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"ajv": "6.15.0",
|
|
47
47
|
"css-tree": "3.2.1",
|
|
48
48
|
"jsdom": "26.1.0",
|
|
49
|
+
"magic-string": "0.30.21",
|
|
50
|
+
"pako": "3.0.2",
|
|
49
51
|
"tailwind-merge": "3.7.0",
|
|
50
52
|
"tailwindcss": "4.3.3",
|
|
51
53
|
"typescript": "5.9.3"
|