inmake DOCS
Docs / Modules / Packages

Packages

The package layout, npm run module:pack, what the check runs, the report, where packages live.

7.1 Layout#

Every module carries its documentation. The package's README.md is the admin-facing guide — what the module does, how to set it up, what it writes and what uninstalling keeps — and docs/{locale}.md adds translations (docs/cs.md, docs/en.md; the store-locale spelling docs/cs_CZ.md is accepted too). The console shows it on the module page's Documentation tab and in the catalog before install, in the reader's console language, falling back to README.md. Markdown only (GitHub-flavoured), ≤ 256 KiB per file, no subfolders under docs/; relative links and images are not resolved in v1 (absolute https: only). Upload rows: docs_missing (error for every target — neither README.md nor a docs/*.md), docs_invalid (a docs/ file not named {locale}.md, or not UTF-8), docs_size, docs_empty (warn — a README under ~200 characters). The starter's module-pack runs the same rows before upload. Served by POST /api/module/docs (§8.6).

One more file may sit in docs/: docs/elements.md, the element guide — the storefront contract for building elements on the module (the runtime API, the /m/{id}/* routes with their shapes, data attributes, events, translation keys, one minimal example). It is not a language file (the {locale}.md rule takes two letters), is served by the same route with kind: 'elements', and is what the agents read through readModuleGuide. A kit that declares a shared script (global.js, or the legacy runtime) without it is docs_elements_missing (warn).

{id}-{version}.zip
├── manifest.json          # ModuleManifest (§2) — the only required file
├── module.wasm            # hosted/platform only (later phase); forbidden for connected and none
├── src/                   # optional TS/JS source; deploy: managed → the function source (src/package.json + its `main`); never in a none package
├── ui/                    # optional static admin pages: .html .js .css .svg .png .woff2 .json only (later phase); never in a none package
├── kit/{kitId}/           # optional element kit(s) in the templates/ format (template.json + elements); required for none
│   ├── global.css         # optional global stylesheet, named by template.json "global.css" (≤ 128 KB) → elements/{id}_runtime css
│   └── global.js          # optional shared storefront script, "global.js" (or the legacy "runtime": "runtime.js"; ≤ 128 KB) → elements/{id}_runtime js
├── templates/{id}.{locale}.hbs    # optional e-mail templates seeded into data/{locale}/templates/{id}; never in a none package
├── translations/{locale}.json     # optional {{t}} keys merged into data/{locale}/translations/{group} — never overwriting
├── schemas/*.json         # the collection schemas the manifest references; never in a none package
├── indexes.json           # deprecated, do not ship — declare `collection.keys` in the manifest
├── README.md              # REQUIRED: the module's documentation (or docs/{locale}.md)
├── docs/{locale}.md       # optional localized documentation: docs/cs.md, docs/en.md, docs/cs_CZ.md
├── docs/elements.md       # optional element guide — expected when a kit ships a global.js / runtime (docs_elements_missing)
└── CHANGELOG.md           # required for a platform-catalog package

An elements module (hosting: 'none', §2.1) ships the subset manifest.json, README.md / docs/, CHANGELOG.md, kit/ and translations/ — module.wasm, src/, ui/, schemas/ and templates/ are static_file errors (§7.3).

README.md, docs/ and LICENSE are allowed at the root; nothing else is. A connected module's package is usually manifest.json alone — the code lives on the module's own host and the package is the contract. When it carries storefront assets, kit/, templates/ and translations/ are the only trees the install reads back (§8.2 step 5c): a translations file is {group: {leaf: "text"}} (a top-level object = one {{t}} group document, nested objects = deeper dotted keys; a top-level "a.b": "text" splits at the first dot, a bare top-level string lands in the module's own {id} group) — the echo package (examples/module-echo/package/) is the smallest complete example.

A deploy: 'managed' package carries its code: src/ is zipped as-is (the src/ prefix stripped) and deployed as a Cloud Functions gen2 HTTP function (§8.7). src/package.json is required and its main must export the handler as module (the functions framework's FUNCTION_TARGET) — an Express app from expressModuleRouter works as it is. Cloud Build runs npm install against the public registry, so a dependency that is not published there (drop-core-server is not) must be bundled into the source; src/ files are capped at 1 MiB each (src_size) and node_modules/ is refused (src_node_modules). The worked example is npm run pack:managed in examples/module-echo/ (esbuild bundles the echo + the module router into one ~22 KB index.js, express stays a real dependency).

7.2 npm run module:pack#

In the vendor starter (scripts/vendor-core/, handed out through docs/VENDOR.md): builds {id}-{version}.zip from a module folder and runs the stage-1 rows it can run locally — validateManifest, kit_lint, the hash chain (packageHashes) — so a developer sees stage 1 before uploading. The zip is what the console's Upload package takes; there is no manifest paste, no manifest_url, no CLI or MCP upload. The loop is edit → pack → upload (below).

Publishing#

Three doors, one pipeline (§7.3 stages 1–3):

Who Target Lands as Then
A platform admin (master / administrator), for a client organization published at once installable in every store of that organization
A client developer (the developer platform claim, a member of the store) organization only in_review a platform admin approves it in the review queue (/platform/modules) → published; rejected → the notes ride the version
A platform admin platform (the catalog) in_review the review queue; visibility: 'all' needs a second person (two-person rule)

The developer door is the console proxy's (admin/server/api-proxy.mjs): it admits /system/module/upload for a developer claim that is a member of the upload's store, refuses a platform target, and stamps x-drop-submission: review + the verified e-mail (submitted_by) — the core parks an organization version in_review only on that header, which the proxy strips from every client request. Store owners without the claim cannot upload.

7.3 What the check runs — stages 1–2#

validatePackage(zip, ctx) (server, module_package.ts) is pure: bytes in, report out. The manifest part is validateManifest in core-lib, so the console pre-flight (fflate + validateManifest in the browser, before the bytes leave) and module:pack emit the same row ids. Every check is one report row {id, level: 'error' | 'warn' | 'info', message, detail?}; any error row → 422 {report} and nothing is written anywhere. Row ids are the contract between the four runners; the ids below are the ones the plan fixes — the rest are named by validateManifest / validatePackage when they land and this table follows.

Stage · check Row id Level Meaning
1 · archive (archive rows) error ≤ 25 MB, ≤ 500 entries; no .., leading / or backslash in a path; no symlinks; no entry outside the allowed top-level set of §7.1
1 · manifest — id / shape manifest_invalid: {field} error parses; MODULE_ID_PATTERN; tags shape; semver strictly greater than every existing version of the key; vendor; name / description free of HTML; config.fields keys unique
1 · manifest — category manifest_invalid: category error category ∈ MODULE_CATEGORIES and, for a later version, equal to the registry record's
1 · manifest — hosting fixed hosting_immutable (detail: {previous, hosting}) error a later version whose hosting differs from the previous version's (any change, incl. to or from none) — update would answer every install 409 hosting_changed (§8.4); a new hosting is a new module id
1 · manifest — hosting by target hosting_target, endpoint_invalid error target: 'platform' requires hosting ∈ {hosted, platform, none} — or connected with an endpoint on a MODULE_PLATFORM_ENDPOINTS origin (a deploy: 'managed' package never qualifies); target: 'organization' allows all four; endpoint required iff connected and passes assertPublicEndpoint. A platform-operated origin in an organization package is endpoint_invalid / admin_page_url with reason platform_only
1 · manifest — grants (manifest row) error access ⊆ MODULE_MAX_ACCESS minus the deny list (§5.1); scope leaves valid for the store entity roots
1 · manifest — collections (manifest row) error every collection named in events[] / data.collections[] has a schema in schemas/
1 · manifest — reserved records name collection_reserved error a records collection named state, outbox or logs (the install doc's reserved subcollections, §5.5), or with a key outside [a-z][a-z0-9_-]{0,40}
1 · manifest — content collection prefix collection_prefix error a content collection not named {id}_{name} (or the bare {id}) — the shared data/{locale}/ namespace stays free of module-taken common names
1 · manifest — entity writes manifest_admin_entity_write warn access grants admin.create / admin.add / admin.update together with data.entity_fields — a reminder that the core refuses create / add / delete on those entities at runtime (§5.4); admin.delete is never in MODULE_MAX_ACCESS
1 · manifest — callbacks (manifest row) error callbacks ⊆ MODULE_CALLBACKS
1 · manifest — admin pages manifest_invalid: admin.pages, admin_page_url error page shape: capability ∈ the mount capabilities, roster a boolean; url on the endpoint origin (connected) or inside ui/ (package)
1 · manifest — uninstall point routes_invalid (detail.field: 'uninstall') error routes.uninstall not a boolean, or true on a non-connected module
1 · manifest — booking policy callbacks_policy_required error callbacks_policy.order_validate must be explicit when category === 'booking'
1 · manifest — provides provider_kind, provider_category, provider_callbacks, provider_services, provider_points_required error the carrier rules of §2.4
1 · manifest — provides gate provider_unsupported error refused when the platform build does not support the kind
1 · manifest — egress hosts egress_host error hostnames only — no scheme, port or IP literal; none of the platform's own (*.inmake.dev, *.inbs.dev, *.googleapis.com, *.firebaseio.com, *.run.app, *.web.app, *.firebaseapp.com, *.cloudfunctions.net — the platform-endpoint allowlist never applies to egress) and never the GCE metadata server (metadata.google.internal, metadata, 169.254.169.254)
1 · manifest — egress wildcard egress_wildcard info one wildcard form is allowed: a single leading * label followed by ≥ 2 literal labels (*.api.mailchimp.com yes; *, *.com, api.*.com, *mailchimp.com no), and the expansion must not match a denied host. The runner's glob also matches multi-label expansions (a.b.api.mailchimp.com) — the info row records that residual for the reviewer
1 · manifest — limits (manifest row) warn a limits value clamped to the tier profile
1 · manifest — public secret (manifest row) error public: true on a secret field
1 · module.wasm wasm_unexpected error present in a connected package — nobody would run it. (For hosted / platform: required, ≤ 4 MiB, compiles, exports call — the runner's one entry point, the point rides x-drop-point (wasm_export_missing) — and imports only the Extism namespaces extism:host/env / extism:host/user: a WASI import is error wasm_import, another namespace warn wasm_import)
1 · ui/ (ui rows) error / warn extension allowlist, ≤ 4 MiB per file, ≤ 16 MiB total; the secret regex (sk_live_|sk_test_|whsec_|BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|AIza[0-9A-Za-z_-]{35}|ya29\.) is an error; an external <script src> / @import / fetch('https://…') literal is a warn
1 · kit/ kit_lint error the element-kit checks of core-lib utils/kit_lint.ts (the format: templates/README.md); every element id starts {moduleId}-
1 · kit/ — runtime kit_runtime_missing, kit_runtime_size error template.json.runtime must name a .js file at the kit root (KIT_RUNTIME_PATTERN, no folders) that the kit carries; ≤ KIT_RUNTIME_MAX_BYTES = 128 KB (same kit_lint)
1 · kit/ — global kit_global_missing, kit_global_size, kit_runtime_size, kit_runtime_conflict error template.json.global = {css?, js?}, each a file at the kit root the kit carries (.css / .js); global.css ≤ KIT_GLOBAL_CSS_MAX_BYTES = 128 KB, global.js ≤ KIT_RUNTIME_MAX_BYTES; global.js and runtime are one slot — declaring both is kit_runtime_conflict (same kit_lint, module kits only). Errors at upload; the mount (install / update / restamp) tolerates these rows on a version published before them — it logs a warning and mounts without the offending part (no global.css; no global.js, the legacy runtime instead) — while every other kit row still refuses the mount
1 · kit/ — assets kit_assets_ignored warn template.json.assets or an assets/ folder — nothing there is mounted; reference external URLs or the store's design system from global.css
1 · manifest — elements kits kits_required error hosting: 'none' only: kits missing, empty, not a string array, or naming a kit more than once (detail.duplicates)
1 · kit/ — elements package kits_missing error hosting: 'none' only: a manifest.kits entry with no kit/{id}/ in the package (a code module's mount skips such a kit with a warning instead)
1 · elements package — files static_file (detail.path) error hosting: 'none' only: the package carries module.wasm, src/, ui/, schemas/ or templates/ — nothing would run, serve or read them
1 · docs/ — element guide docs_elements_missing warn a kit declares a shared script (global.js, or the legacy runtime) but the package has no docs/elements.md — the guide others build elements from (§7.1); global.css alone needs no guide
1 · manifest — console pages admin_pages_missing warn the module has a console side (routes.admin or a records collection) but declares no admin.pages[] — every such module gives the store at least one page: it is what the Data page lists first for the module and what a manager pins to the Home menu (Pin to Home on the page, the module's Pages card and the Data row)
1 · templates/, translations/ (template rows) error Handlebars compiles (a stub {{t}} registered), locale ids xx_YY, translation values are strings
1 · schemas/, indexes.json (schema rows), indexes_deprecated error / warn SchemaEntity shape (the same guard writeSchema runs); a collection.keys map is validated with core-lib's validateKeys (≤ 8 keys, 1–4 fields, names ^[a-z][a-z0-9_]{0,31}$, no id / _k — error); data.indexes / indexes.json is warn indexes_deprecated — keys replace composites
1 · CHANGELOG.md (changelog row) error / info required for the platform target with a heading for manifest.version; info when absent from an organization package
1 · hashes — — sha256 per file, wasm_hash, package_hash = sha256 over the sorted "{path}\0{sha256}\n" lines — computed server-side from the received bytes; a hash in the request is ignored
2 · policy scan (policy rows) warn the secret regex over every text file; a high-entropy token (≥ 40 base64 chars in a string literal in .js / .json); binaries only where allowed; src/ without node_modules/ and no file above 1 MiB; a literal hostname in src/ (or the wasm's data segments) missing from egress is a warn ("names a host it may not call"), an egress host that appears nowhere is info
2 · kit egress kit_egress warn a host literal in any kit/**/*.js (element scripts and the runtime — code that runs first-party in the shopper's browser) that the manifest's egress does not cover; XML namespace URIs (www.w3.org) are ignored. Shown to the reviewer

Stage 3 — the dry run: hosted / platform packages are instantiated on the dedicated module-runner-check service (never a service that carries live install tokens) with a placeholder token minted for the check (access: {}, scope: {}, revoked afterwards) and egress: []; install {dry_run: true} and verify run under a 10 s wall clock, then one synthetic envelope per other declared point. The rows: stage3_instantiate (the module does not instantiate), install_trap, install_fuel (> 50 % of the clamped install budget), verify_trap, platform_path_denied (a platform_fetch path outside the route tree the install token will carry — access ∩ the ceiling plus the report-back routes a provides kind earns, installAccessFor in module_rules.ts; an invoicer's module/invoice/pending passes, a manifest's own access.module never does), egress_attempted (a host not in egress) — all error; synthetic_trap (warn); runner_unavailable (error — no check service in the env, or it did not answer: retry the upload). The report's measured block carries instantiate_ms, fuel / time per call and platform_paths_attempted. The objects of a version that fails stage 3 are deleted again (no record was written). A connected package has no stage 3 — its endpoint is proven at install (§4.1) — and its report carries stage_reached: 2; neither has an elements package (hosting: 'none': there is no code to run), whose upload and re-check never reach the runner (Registry.upload / Registry.check dry-run hosted / platform only).

Stage 4 — review: a platform-catalog version is in_review after stage 3 and a platform admin approves or rejects it on /platform/modules (visibility: 'all' needs a second person — the reviewer must not be the submitter, 403 two_person_rule). An organization package is published at once.

7.4 The report#

interface ModuleCheckReport {
    ok: boolean;                                 // no 'error' rows
    at: number;
    stage_reached: 1 | 2 | 3;                    // 2 for a connected or an elements (none) package
    elements_count?: number;                     // the listed kits' element count (sum of template.json order[] lengths)
    checks: Array<{id: string; level: 'error' | 'warn' | 'info'; message: string; detail?: Entity}>;
    measured?: {…};                              // the dry run's numbers (later phase)
    hashes: {package: string; wasm?: string;
             files?: Record<string, string>;     // in the upload RESPONSE only
             files_count?: number};              // what the registry record keeps
    diff?: {against: string; manifest: string[]; grants_widened: boolean; egress_added: string[];
            schema_fields_removed: string[]; files_changed: string[]; files_added: string[]; files_removed: string[]};
}

diff compares with the previous version of the same registry key — its highest version of any state, against that version's own manifest snapshot (every registry version record carries its manifest, immutable like the bytes). The report is stored on the registry version record without hashes.files (the per-file map is in the upload response only, shown once by the console, and in the .hashes.json object beside the package — the next upload's file-diff base; the record keeps hashes.files_count), copied onto ModuleInstall.report at install / update, and rendered by the console's ModuleCheckReportCard (rows grouped by level, the diff with the old → new grant trees). elements_count is set whenever the manifest lists kits — the sum of the listed kits' template.json order[] lengths (an unlisted kit does not count); it survives the summary, so the catalog's versions[].report carries it for the Elements chip.

7.5 Where packages live#

Objects are written create-only to the env's core bucket (modules/platform/{id}/{version}/… or modules/org/{org}/{id}/{version}/…) plus one registry record modules/{key}. manifest.json is written first, so a second upload of the same version fails on the first write with 409 version_exists and nothing is overwritten; a .hashes.json sidecar (the per-file map, the next upload's file-diff base) is written beside it. A published version's bytes never change — a bad one is yanked, not patched. Every version record on the registry carries its own manifest snapshot and its report summary, so the catalog, the diff base and the unpublish snap-back never read the bucket. The install / update does read it — once: it lists the version's prefix and downloads only the named kits' trees, the named templates' .hbs files and every translations/*.json (never src/, ui/, schemas/), then writes the mounts onto the store's own cloud (§8.2).