@assembly-js/app-foundation 0.0.41 → 0.0.43
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/CHANGELOG.md +172 -0
- package/CLAUDE.md +279 -23
- package/bin/check-paint-beacon.mjs +8 -9
- package/bin/check-proxy-csrf.mjs +387 -0
- package/dist/proxy.d.ts +11 -0
- package/dist/proxy.js +669 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/fixes.json +64 -0
- package/package.json +5 -3
package/dist/server.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './labels.js';
|
|
|
12
12
|
export * from './notifications.js';
|
|
13
13
|
export * from './cron.js';
|
|
14
14
|
export * from './replace-rows.js';
|
|
15
|
+
export * from './proxy.js';
|
|
15
16
|
export * from './api-errors.js';
|
|
16
17
|
export * from './params.js';
|
|
17
18
|
export * from './client.js';
|
package/dist/server.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './labels.js';
|
|
|
16
16
|
export * from './notifications.js';
|
|
17
17
|
export * from './cron.js';
|
|
18
18
|
export * from './replace-rows.js';
|
|
19
|
+
export * from './proxy.js';
|
|
19
20
|
// Returns NextResponse, so route handlers only — but nothing here reads a
|
|
20
21
|
// secret, which is why a client file may still `import type` from them.
|
|
21
22
|
export * from './api-errors.js';
|
package/fixes.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "next-config-wrapper",
|
|
4
|
+
"version": "0.0.20",
|
|
5
|
+
"title": "Your app's live preview no longer hangs on networks that block its dev connection.",
|
|
6
|
+
"check": "Your app's Next config must be split so next.config.ts is the platform-owned wrapper and your own settings live in app.config.ts. The end state: next.config.ts contains exactly:\n\nimport { withAssemblyDev } from \"@assembly-js/app-foundation/next\";\nimport appConfig from \"./app.config\";\n\nexport default withAssemblyDev(appConfig);\n\nTo get there: create app.config.ts and move whatever next.config.ts currently exports (serverExternalPackages, experimental flags, redirects, headers, etc.) into it verbatim, keeping the export shape the app already used (e.g. `export default { ... }` or `export default appConfig satisfies NextConfig`). Then replace next.config.ts's contents with the wrapper above. If a sibling next.config.js or next.config.mjs exists, delete it — Next loads whichever config file it finds first, and a leftover unwrapped copy would silently run instead of the wrapper. Do not add new Next options while you're at it, and do not touch anything outside these two files — this fix is a relocation, not a config change.",
|
|
7
|
+
"detector": {
|
|
8
|
+
"absent": ["next.config.js", "next.config.mjs"],
|
|
9
|
+
"present": [
|
|
10
|
+
{ "path": "next.config.ts", "contains": ["withAssemblyDev("] },
|
|
11
|
+
{ "path": "app.config.ts" }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "paint-beacon",
|
|
17
|
+
"version": "0.0.22",
|
|
18
|
+
"title": "Your app's preview now accurately reports when a page has finished rendering, not just loaded.",
|
|
19
|
+
"check": "Your app is missing the post-paint beacon the builder preview uses to tell \"the page executed\" from \"the page actually rendered.\" Add the component exactly as the foundation ships it, at app/components/paint-beacon.tsx:\n\n'use client';\n\nimport { useEffect } from 'react';\nimport { installPaintBeacon } from '@assembly-js/app-foundation/client';\n\nexport default function PaintBeacon(): null {\n useEffect(() => {\n installPaintBeacon();\n }, []);\n return null;\n}\n\nRender it once, self-closing and prop-less, in the root layout's <body> (app/layout.tsx): import it as `import PaintBeacon from '@/app/components/paint-beacon';` and add `<PaintBeacon />` alongside the other foundation-owned providers already rendered there.\n\nThe beacon's POST must be routed before any CSRF or session-exchange handling, because sendBeacon is a headerless POST that 403s on the normal path. The platform sign-in proxy already does this, so proxy.ts's proxy export should be the package's:\n\nimport { createAssemblyProxy } from '@assembly-js/app-foundation/server';\n\nexport const proxy = createAssemblyProxy();\n\nIf proxy.ts has been rewritten away from that, restore that export (keep the file's `config` matcher as it is) rather than adding a beacon route by hand. Make sure @assembly-js/app-foundation is a declared dependency (it should already be installed). Do not post app.painted or hit /__assembly/painted yourself — the component already does both.",
|
|
20
|
+
"detector": {
|
|
21
|
+
"present": [
|
|
22
|
+
{ "path": "app/components/paint-beacon.tsx" },
|
|
23
|
+
{ "path": "app/layout.tsx", "contains": ["<PaintBeacon"] }
|
|
24
|
+
],
|
|
25
|
+
"dependencies": ["@assembly-js/app-foundation"],
|
|
26
|
+
"checks": [{ "of": "@assembly-js/app-foundation", "script": "check-paint-beacon.mjs" }]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "inlined-child-rows-batch",
|
|
31
|
+
"version": "0.0.37",
|
|
32
|
+
"title": "Saving a record with nested rows no longer fails and drops the rows.",
|
|
33
|
+
"check": "Your app has its own copy of the child-row replace helper, and that copy calls drizzle's batch detached from db (`const batch = db.batch …; await batch([...])`). drizzle's batch reads this.session, so in a deployed app every save that writes nested rows (a proposal's packages, a retainer's allowances) fails with a 500 and leaves the parent without its children.\n\nDelete the app's copy and import the package's helper instead: `import { replaceChildRows } from '@assembly-js/app-foundation/server';`. Its signature is the one the copy was made from, so call sites keep their arguments. If the copy has drifted from the original and can't be swapped cleanly, keep it and change only the call to `await batch.call(db, [first, ...rest]);`. Do not change what the helper writes or the order it writes in.",
|
|
34
|
+
"detector": {
|
|
35
|
+
"forbidden": [{ "id": "detached-db-batch", "pattern": "(^|[^.\\w])batch\\(\\[" }]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "identity-outage-states",
|
|
40
|
+
"version": "0.0.40",
|
|
41
|
+
"title": "If Assembly can't confirm who is signed in, your app now shows a retry screen instead of an error.",
|
|
42
|
+
"check": "Since 0.0.40, getSessionIdentity() asks the platform who the viewer is and throws an AuthUnavailableError when the platform can't be reached. Your root layout calls it without a catch, so during an outage every page fails instead of showing a retry screen, and every failed action renews the session without a bound. Bring three app-owned files to the foundation's current shape:\n\n1. Create app/components/auth-unavailable.tsx:\n\n'use client';\n\nexport default function AuthUnavailable() {\n return (\n <main className=\"flex min-h-screen flex-col items-center justify-center gap-3 px-4 text-center\">\n <p className=\"text-sm text-muted-foreground\">\n We couldn't load this right now. Please try again.\n </p>\n <button\n type=\"button\"\n className=\"rounded-md border px-3 py-1.5 text-sm\"\n onClick={() => window.location.reload()}\n >\n Try again\n </button>\n </main>\n );\n}\n\n2. In app/layout.tsx, import it (`import AuthUnavailable from '@/app/components/auth-unavailable';`) and replace `const identity = await getSessionIdentity();` with:\n\nlet identity: Awaited<ReturnType<typeof getSessionIdentity>> = null;\nlet authUnavailable = false;\ntry {\n identity = await getSessionIdentity();\n} catch (err) {\n if (!(err instanceof Error && err.name === 'AuthUnavailableError')) throw err;\n authUnavailable = true;\n}\nif (authUnavailable) {\n return (\n <html lang=\"en\" style={{ colorScheme: 'light' }}>\n <body className=\"font-sans antialiased\">\n <AuthUnavailable />\n </body>\n </html>\n );\n}\n\nKeep everything else in the layout as it is.\n\n3. Create app/components/renewal-gate.ts:\n\nexport function createRenewalGate(\n renew: () => Promise<boolean>,\n windowMs: number,\n now: () => number = Date.now,\n): () => Promise<boolean> {\n let inFlight: Promise<boolean> | null = null;\n let lastStartedAt = Number.NEGATIVE_INFINITY;\n return () => {\n if (inFlight) return inFlight;\n if (now() - lastStartedAt < windowMs) return Promise.resolve(false);\n lastStartedAt = now();\n inFlight = renew().finally(() => {\n inFlight = null;\n });\n return inFlight;\n };\n}\n\nThen in app/components/assembly-context.tsx add `import { createRenewalGate } from './renewal-gate';`, define `const renewOnce = createRenewalGate(reexchangeBearer, 30_000);` at module scope, and change the retry path's `await reexchangeBearer()` to `await renewOnce()`. Change nothing else in that file.",
|
|
43
|
+
"detector": {
|
|
44
|
+
"present": [
|
|
45
|
+
{ "path": "app/components/auth-unavailable.tsx" },
|
|
46
|
+
{ "path": "app/layout.tsx", "contains": ["AuthUnavailableError"] },
|
|
47
|
+
{ "path": "app/components/renewal-gate.ts" },
|
|
48
|
+
{ "path": "app/components/assembly-context.tsx", "contains": ["createRenewalGate("] }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "package-proxy",
|
|
54
|
+
"version": "0.0.42",
|
|
55
|
+
"title": "Your app's sign-in and cross-site request protection now come from Assembly and are tested by it.",
|
|
56
|
+
"check": "Your app's sign-in proxy is still code the app owns. Since 0.0.42 the package provides all of it — the token exchange, the bearer cookie, the CSRF gate and the re-exchange pass-through — and proxy.ts only calls it:\n\nimport { createAssemblyProxy } from '@assembly-js/app-foundation/server';\n\nexport const proxy = createAssemblyProxy();\n\nReplace the proxy function in proxy.ts with that export and delete a root middleware.ts if one exists. Keep the file's `config` matcher, including any exclusions the app added for its own paths; `api/assembly-reexchange` must stay excluded. Carry over only what the app added on top of the foundation's proxy: request headers it set for its own pages go in `createAssemblyProxy({ onForward({ request, headers, session }) { headers.set(…) } })`. Do not keep, port or rewrite any token, cookie or CSRF handling. Verify with `node node_modules/@assembly-js/app-foundation/bin/check-proxy-csrf.mjs --strict`; if it fails, the fix is the file above, not a change to request handling.",
|
|
57
|
+
"detector": {
|
|
58
|
+
"absent": ["middleware.ts"],
|
|
59
|
+
"present": [{ "path": "proxy.ts", "contains": ["@assembly-js/app-foundation/server", "createAssemblyProxy("] }],
|
|
60
|
+
"dependencies": ["@assembly-js/app-foundation"],
|
|
61
|
+
"checks": [{ "of": "@assembly-js/app-foundation", "script": "check-proxy-csrf.mjs" }]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assembly-js/app-foundation",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.43",
|
|
4
4
|
"description": "Versioned foundation helpers and build context for Assembly generated apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"bin",
|
|
9
9
|
"skills",
|
|
10
10
|
"CLAUDE.md",
|
|
11
|
-
"render-context.mjs"
|
|
11
|
+
"render-context.mjs",
|
|
12
|
+
"fixes.json",
|
|
13
|
+
"CHANGELOG.md"
|
|
12
14
|
],
|
|
13
15
|
"exports": {
|
|
14
16
|
"./server": {
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
},
|
|
31
33
|
"scripts": {
|
|
32
34
|
"build": "tsc && tsc-alias",
|
|
33
|
-
"test": "bun test && tsc -p tsconfig.replace-rows.json",
|
|
35
|
+
"test": "npm run build && bun test --path-ignore-patterns='**/proxy.test.ts' && bun test ./src/proxy.test.ts && tsc -p tsconfig.replace-rows.json",
|
|
34
36
|
"prepublishOnly": "npm run build && npm run verify:dist && npm run verify:bin",
|
|
35
37
|
"verify:dist": "node scripts/verify-dist-resolves.mjs",
|
|
36
38
|
"verify:bin": "node scripts/verify-bin.mjs"
|