6. Bundle build, signing, and catalog submission
Build pipeline (recommended)
- Develop UI with
@nt2/vault-sdk(createVaultClient); pinsdkVersioninmanifest.jsonto the same semver. - Bundle static assets — no remote runtime dependencies.
- Compute
entry.integrityover the exact entry bytes shipped. - Sign canonical manifest JSON with your Vault Key DID private key; set
publisher+signature. - Zip as
{id}-{version}.nt2app. - Test install in dev/staging vault with
microAppsfeature enabled. - Self-review using Appendix A.
Catalog entry
Official listings use MicroAppCatalogEntry shape:
| Field | Requirement |
|---|---|
publisherKeyDid, signPubJwk, catalogSignature | Catalog row signed with your Vault Key DID |
bundleUrl | HTTPS GitHub Release browser download URL (v1) — see below |
bundleIntegrity | sha384-… of the full .nt2app zip file at bundleUrl |
permissions, description | Shown in install consent — explain each slug |
The host fetches catalog JSON when online; after install the app runs from the local bundle only.
Your description must explain what the app does and why each permission is requested — this text appears in the install consent flow.
GitHub Release publishing (recommended for OSS)
- Build and sign
{id}-{version}.nt2app(same artifact used for sideload). - Create a GitHub Release (tag e.g.
v1.2.0aligned withmanifest.version). - Attach the
.nt2appas a release asset on a public repository. - Copy the browser download URL (not
api.github.com):
https://github.com/{owner}/{repo}/releases/download/v1.2.0/{id}-{version}.nt2app - Compute
bundleIntegrity: SHA-384 over the zip file bytes (sha384-+ base64), same algorithm asentry.integrity. - Open a PR on the community catalog adding
catalog/entries/{id}.jsonwithbundleUrl,bundleIntegrity, and yourcatalogSignatureover the canonical entry (CONTRIBUTING).
Do not point manifest.entry at a remote URL — the iframe loads only from the installed local copy after fetch. If GitHub is unreachable at install time, users can still sideload the same .nt2app file manually.
What reviewers reject
- Unused or overly broad permissions
- Missing or invalid SRI / signature
- Remote script loading or third-party analytics
- Fake vault UI or kernel copy
- Raw SDK error codes shown to users
- Broken
manifest.routesor entry integrity drift
Open a PR on nt2-community/micro-apps-catalog — see CONTRIBUTING on GitHub.
Published catalog URLs and site map: community hub · micro-apps catalog.
Appendix A — Catalog submission checklist
Use this checklist before submitting to the NT² catalog or an internal pilot review. Every MUST item must pass.
Shell & UX
- A1 — No parent-frame navigation or DOM access
- A2 — Locked state clears secrets and shows safe UI
- A3 — No imitation of NT² unlock / Settings / shell chrome
- A4 —
@nt2/vault-sdkclient (or Appendix C wire protocol for prototypes only); nowindow.parent.*hacks - B1 — Mobile-first; ≥ 44×44 px tap targets
- B2 —
dvh+ safe-area insets on full-height layouts - B3 — No
window.confirm/alert/prompt - B4 — Destructive actions require in-app confirm
- B5 — Fully offline-capable after install
- B6 — Respects
prefers-reduced-motion - C1 — No CDN fonts/CSS
- C2 — No analytics or tracking
- C3 — Visual scheme does not clash with vault dark chrome
- D1 — Minimum permissions only
- D2 — Permission rationale in catalog + in-app copy
- D3 — SDK errors mapped to friendly messages
- D4 — Graceful
PERMISSION_DENIEDhandling - E1 — Routes stay under
/apps/{appId}/… - E2 — External links labeled +
noopener noreferrer - E3 —
manifest.routesmatches implemented routes - F1 — Visible keyboard focus
- F2 — WCAG AA contrast
- F3 — No hover-only critical actions
- G1 — Loading skeleton/spinner on mount
- G2 — SDK timeout handling
- G3 — Meaningful empty states
Security
- H1 — Vault data access via SDK only
- H2 — No key export attempts
- H3 — No remote JavaScript at runtime
- H4 — No nested cross-origin iframes
- H5 — No secrets in SDK request payloads
- I1 — Manifest lists only used permissions
- I2 — Per-slug rationale documented
- I3 — No wildcard permissions
- J1 — No sensitive console logging in production build
- J2 — No external transmission of vault data
- J3 — Secrets cleared on lock
- J4 — No embedded API keys
- K1 — No persistent plaintext cache
- K2 — Clipboard only on user gesture
- K3 —
LOCKEDhalts in-flight calls - L1 — Valid SHA-384
entry.integrity - L2 — Manifest signed with publisher Key DID
- L3 —
sdkVersionpinned;PROTOCOL_MISMATCHhandled in UI - L4 — Protocol package not outdated vs host
- M1 —
NOT_WRITERhandled without retry loops - M2 — Writer status not assumed
- N1 — Client validation before SDK writes
- N2 — User content sanitized before DOM insert
Metadata
- App
idis stable and unique -
versionsemver incremented from prior submission -
bundleUrlis a public GitHub Release browser download URL -
bundleIntegritymatches SHA-384 of the release.nt2appfile -
CHANGELOG.md(or release notes) attached for reviewers - Tested on PWA; Tauri matrix recommended before wide catalog publish
Appendix B — SDK error handling
Map every error to user-facing copy and app behavior. Never display raw code strings in the UI.
| Code | User-facing message (example) | App behavior |
|---|---|---|
LOCKED | “Vault is locked. Unlock in the main app to continue.” | Clear secrets; disable writes; show locked panel |
NOT_WRITER | “Another tab is editing this vault. Switch tabs and try again.” | Show retry button; do not loop |
PERMISSION_DENIED | “This app doesn’t have access to that data.” | Empty state; no crash |
PROTOCOL_MISMATCH | “This app needs an update for your vault version.” | Disable writes; link to update/docs |
BAD_REQUEST | “Check the highlighted fields and try again.” | Inline validation |
NOT_FOUND | “That item is no longer available.” | Refresh list |
NOT_INSTALLED | “App is not installed.” | (Usually host-level — show reinstall hint) |
INTEGRITY | “Installation is corrupted. Reinstall the app.” | Block usage; prompt reinstall |
INTERNAL | “Something went wrong. Try again.” | Log internally in dev only; offer retry |
Using VaultSdkError in TypeScript
When you use @nt2/vault-sdk, failed RPCs throw VaultSdkError with code, message, and requestId. Timeouts throw VaultSdkTimeoutError.
import { createVaultClient, VaultSdkError, VaultSdkTimeoutError } from '@nt2/vault-sdk';
try {
await client.items.forCategory('note').create({ title: 'Memo' });
} catch (error) {
if (error instanceof VaultSdkTimeoutError) {
showToast('Vault did not respond. Try again.');
return;
}
if (error instanceof VaultSdkError) {
switch (error.code) {
case 'LOCKED':
showLockedPanel();
break;
case 'PERMISSION_DENIED':
showEmptyState();
break;
default:
showRetry(error.message);
}
}
} Use instanceof VaultSdkError — do not parse raw ERR envelopes in application code when the SDK client is available.
flowchart TD CALL["SDK call"] OK["OK → render"] LOCKED["LOCKED → clear + locked UI"] NW["NOT_WRITER → warning + retry"] PD["PERMISSION_DENIED → empty state"] PM["PROTOCOL_MISMATCH → update message"] CALL --> OK CALL --> LOCKED CALL --> NW CALL --> PD CALL --> PM
Appendix C — Advanced wire protocol
Use this only when you cannot bundle @nt2/vault-sdk (e.g. a zero-build static HTML prototype). Production catalog apps should prefer the typed client (§2).
Wire envelopes are defined in @nt2/vault-sdk-protocol. Pattern:
- Generate a UUID
idper request. window.parent.postMessage({ protocolVersion: 1, id, type, … }, '*').- Listen for
{ id, type: 'OK' | 'ERR', … }onwindow.addEventListener('message', …). - Enforce a timeout (e.g. 10 s) so the UI never hangs.
Import PROTOCOL_VERSION, isVaultSdkResponse, and request/response types from @nt2/vault-sdk-protocol in TypeScript projects — do not hard-code a stale protocol integer.