toiljs 0.0.55 → 0.0.57
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 +10 -0
- package/README.md +72 -14
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +293 -142
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.js +1 -1
- package/build/client/components/Image.d.ts +1 -1
- package/build/client/dev/devtools.js +4 -2
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +2 -2
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/hooks.js +2 -2
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +1 -1
- package/build/compiler/seo.js +1 -3
- package/build/compiler/template-build.d.ts +5 -2
- package/build/compiler/template-build.js +19 -7
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/cache.js +0 -0
- package/build/devserver/crypto.js +45 -17
- package/build/devserver/database.d.ts +1 -1
- package/build/devserver/database.js +84 -0
- package/build/devserver/email/caps.js +0 -0
- package/build/devserver/email/config.js +7 -2
- package/build/devserver/email/validate.js +1 -4
- package/build/devserver/host.js +18 -1
- package/build/devserver/index.d.ts +1 -1
- package/build/devserver/index.js +3 -2
- package/build/devserver/module.js +51 -12
- package/build/devserver/proxy.js +2 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +5 -5
- package/build/io/codec.js +193 -77
- package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
- package/examples/basic/client/public/images/logo.svg +37 -34
- package/examples/basic/client/public/index.html +14 -14
- package/examples/basic/client/routes/auth.tsx +18 -10
- package/examples/basic/client/routes/cookies.tsx +15 -24
- package/examples/basic/client/routes/crypto.tsx +4 -5
- package/examples/basic/client/routes/features/template/template.tsx +1 -1
- package/examples/basic/client/routes/hello.tsx +1 -1
- package/examples/basic/client/routes/pq.tsx +14 -14
- package/examples/basic/client/routes/rest.tsx +1 -3
- package/examples/basic/client/styles/main.css +25 -22
- package/examples/basic/client/toil.tsx +1 -1
- package/examples/basic/server/README.md +8 -8
- package/examples/basic/server/core/AppHandler.ts +4 -7
- package/examples/basic/server/routes/Auth.ts +13 -10
- package/examples/basic/server/routes/EnvDemo.ts +9 -3
- package/examples/basic/server/routes/Guestbook.ts +2 -4
- package/package.json +26 -26
- package/src/backend/index.ts +4 -2
- package/src/cli/create.ts +19 -4
- package/src/cli/diagnostics.ts +48 -0
- package/src/cli/doctor.ts +155 -9
- package/src/cli/notify.ts +1 -6
- package/src/cli/ui.ts +3 -3
- package/src/cli/version-check.ts +5 -1
- package/src/client/auth.ts +33 -10
- package/src/client/components/Form.tsx +2 -2
- package/src/client/components/Image.tsx +1 -1
- package/src/client/components/Script.tsx +1 -1
- package/src/client/components/Slot.tsx +1 -1
- package/src/client/dev/devtools.tsx +126 -55
- package/src/client/dev/error-overlay.tsx +7 -1
- package/src/client/head/metadata.ts +1 -1
- package/src/client/index.ts +13 -2
- package/src/client/routing/Router.tsx +2 -2
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/hooks.ts +5 -3
- package/src/client/routing/loader.ts +2 -2
- package/src/client/routing/mount.tsx +5 -6
- package/src/compiler/docs.ts +1 -1
- package/src/compiler/email-preview.ts +1 -1
- package/src/compiler/generate.ts +1 -1
- package/src/compiler/seo.ts +1 -3
- package/src/compiler/ssg.ts +10 -4
- package/src/compiler/template-build.ts +43 -11
- package/src/compiler/template.ts +1 -4
- package/src/compiler/vite.ts +1 -1
- package/src/devserver/cache.ts +0 -0
- package/src/devserver/crypto.ts +140 -51
- package/src/devserver/database.ts +168 -9
- package/src/devserver/dotenv.ts +10 -2
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +8 -2
- package/src/devserver/email/index.ts +3 -3
- package/src/devserver/email/validate.ts +1 -4
- package/src/devserver/envelope.ts +3 -3
- package/src/devserver/host.ts +46 -6
- package/src/devserver/index.ts +15 -6
- package/src/devserver/module.ts +56 -14
- package/src/devserver/proxy.ts +5 -7
- package/src/io/codec.ts +226 -83
- package/test/devserver-database.test.ts +60 -0
- package/test/devserver-secrets.test.ts +59 -0
- package/test/doctor.test.ts +30 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
2
2
|
import { DataReader, DataWriter } from 'data';
|
|
3
|
-
import { Record } from 'toildb';
|
|
4
3
|
|
|
5
4
|
import { encodeSessionUser } from './Session';
|
|
6
5
|
|
|
@@ -64,11 +63,6 @@ function deriveSalt(username: string): Uint8Array {
|
|
|
64
63
|
return crypto.sha256Text('toil-demo-salt-v1:' + username).slice(0, 16);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
// ToilDB collections (the `kv.*` dev placeholder is gone). The key + value are
|
|
69
|
-
// `@data` types: the binary codec is generated, the host marshals it, and the
|
|
70
|
-
// challenge is consumed exactly once with `getDelete`.
|
|
71
|
-
|
|
72
66
|
@data
|
|
73
67
|
class Username {
|
|
74
68
|
name: string = '';
|
|
@@ -106,8 +100,8 @@ class Challenge {
|
|
|
106
100
|
|
|
107
101
|
@database
|
|
108
102
|
class AuthDb {
|
|
109
|
-
@collection accounts
|
|
110
|
-
@collection challenges
|
|
103
|
+
@collection static accounts: Documents<Username, AuthAccount>;
|
|
104
|
+
@collection static challenges: Documents<ChallengeId, Challenge>;
|
|
111
105
|
}
|
|
112
106
|
|
|
113
107
|
@rest('auth')
|
|
@@ -237,8 +231,17 @@ class Auth {
|
|
|
237
231
|
const acct = AuthDb.accounts.get(new Username(ch.username));
|
|
238
232
|
if (acct == null) return fail();
|
|
239
233
|
const message = AuthService.buildLoginMessage(
|
|
240
|
-
ch.username,
|
|
241
|
-
|
|
234
|
+
ch.username,
|
|
235
|
+
AUD,
|
|
236
|
+
cid,
|
|
237
|
+
ch.nonce,
|
|
238
|
+
ch.iat,
|
|
239
|
+
ch.exp,
|
|
240
|
+
ct,
|
|
241
|
+
DEMO_MEM_KIB,
|
|
242
|
+
DEMO_ITERS,
|
|
243
|
+
DEMO_PAR,
|
|
244
|
+
AuthService.serverKemKeyId()
|
|
242
245
|
);
|
|
243
246
|
if (!AuthService.verifyLogin(acct.publicKey, message, sig)) return fail();
|
|
244
247
|
|
|
@@ -34,9 +34,15 @@ class EnvDemo {
|
|
|
34
34
|
const apiKeySet = Environment.getSecure('DEMO_API_KEY') != null;
|
|
35
35
|
|
|
36
36
|
const body =
|
|
37
|
-
'PUBLIC_GREETING=' +
|
|
38
|
-
|
|
39
|
-
'
|
|
37
|
+
'PUBLIC_GREETING=' +
|
|
38
|
+
greeting +
|
|
39
|
+
'\n' +
|
|
40
|
+
'REGION=' +
|
|
41
|
+
region +
|
|
42
|
+
'\n' +
|
|
43
|
+
'DEMO_API_KEY set=' +
|
|
44
|
+
(apiKeySet ? 'yes' : 'no') +
|
|
45
|
+
'\n';
|
|
40
46
|
return Response.text(body, 200);
|
|
41
47
|
}
|
|
42
48
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Counter, Events } from 'toildb';
|
|
2
|
-
|
|
3
1
|
import { GuestEntry } from '../models/GuestEntry';
|
|
4
2
|
import { GuestbookView } from '../models/GuestbookView';
|
|
5
3
|
import { NewMessage } from '../models/NewMessage';
|
|
@@ -28,8 +26,8 @@ class GuestKey {
|
|
|
28
26
|
|
|
29
27
|
@database
|
|
30
28
|
class GuestbookDb {
|
|
31
|
-
@collection entries
|
|
32
|
-
@collection totals
|
|
29
|
+
@collection static entries: Events<GuestKey, GuestEntry>;
|
|
30
|
+
@collection static totals: Counter<GuestKey>;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/** The current total + the 10 newest entries. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toiljs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.57",
|
|
5
5
|
"author": "Dacely",
|
|
6
6
|
"description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
|
|
7
7
|
"repository": {
|
|
@@ -120,21 +120,21 @@
|
|
|
120
120
|
"@dacely/noble-post-quantum": "^0.6.1",
|
|
121
121
|
"@noble/curves": "^2.2.0",
|
|
122
122
|
"@dacely/toilscript-loader": "^0.1.0",
|
|
123
|
-
"@eslint-react/eslint-plugin": "^5.
|
|
123
|
+
"@eslint-react/eslint-plugin": "^5.9.0",
|
|
124
124
|
"@eslint/js": "^10.0.1",
|
|
125
|
-
"@typescript-eslint/utils": "^8.
|
|
125
|
+
"@typescript-eslint/utils": "^8.61.1",
|
|
126
126
|
"@vitejs/plugin-react": "^6.0.2",
|
|
127
127
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
128
|
-
"eslint-plugin-react-refresh": "^0.5.
|
|
128
|
+
"eslint-plugin-react-refresh": "^0.5.3",
|
|
129
129
|
"hash-wasm": "^4.12.0",
|
|
130
|
-
"juice": "^12.1.
|
|
131
|
-
"nodemailer": "^9.0.
|
|
130
|
+
"juice": "^12.1.1",
|
|
131
|
+
"nodemailer": "^9.0.1",
|
|
132
132
|
"picocolors": "^1.1.1",
|
|
133
|
-
"sharp": "^0.35.
|
|
134
|
-
"toilscript": "^0.1.
|
|
135
|
-
"typescript-eslint": "^8.
|
|
136
|
-
"vite": "^8.0.
|
|
137
|
-
"vite-imagetools": "^10.0.
|
|
133
|
+
"sharp": "^0.35.1",
|
|
134
|
+
"toilscript": "^0.1.29",
|
|
135
|
+
"typescript-eslint": "^8.61.1",
|
|
136
|
+
"vite": "^8.0.16",
|
|
137
|
+
"vite-imagetools": "^10.0.1",
|
|
138
138
|
"vite-plugin-node-polyfills": "^0.28.0"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
@@ -145,34 +145,34 @@
|
|
|
145
145
|
"typescript": ">=6.0.0"
|
|
146
146
|
},
|
|
147
147
|
"devDependencies": {
|
|
148
|
-
"@babel/core": "^
|
|
149
|
-
"@babel/preset-env": "^
|
|
150
|
-
"@babel/preset-typescript": "^
|
|
148
|
+
"@babel/core": "^8.0.1",
|
|
149
|
+
"@babel/preset-env": "^8.0.1",
|
|
150
|
+
"@babel/preset-typescript": "^8.0.1",
|
|
151
151
|
"@btc-vision/as-covers-assembly": "^0.4.5",
|
|
152
152
|
"@btc-vision/as-covers-transform": "^0.4.5",
|
|
153
153
|
"@btc-vision/as-pect-assembly": "^8.3.0",
|
|
154
154
|
"@btc-vision/as-pect-cli": "^8.3.0",
|
|
155
155
|
"@btc-vision/as-pect-transform": "^8.3.0",
|
|
156
|
-
"@clack/prompts": "^1.5.
|
|
156
|
+
"@clack/prompts": "^1.5.1",
|
|
157
157
|
"@microsoft/api-extractor": "7.58.9",
|
|
158
158
|
"@testing-library/dom": "^10.4.1",
|
|
159
159
|
"@testing-library/react": "^16.3.2",
|
|
160
|
-
"@types/node": "^
|
|
160
|
+
"@types/node": "^26.0.0",
|
|
161
161
|
"@types/nodemailer": "^8.0.1",
|
|
162
|
-
"@types/react": "^19.2.
|
|
162
|
+
"@types/react": "^19.2.17",
|
|
163
163
|
"@types/react-dom": "^19.2.3",
|
|
164
|
-
"@vitest/coverage-v8": "^4.1.
|
|
165
|
-
"@vitest/ui": "^4.1.
|
|
166
|
-
"esbuild": "^0.28.
|
|
167
|
-
"eslint": "^10.
|
|
164
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
165
|
+
"@vitest/ui": "^4.1.9",
|
|
166
|
+
"esbuild": "^0.28.1",
|
|
167
|
+
"eslint": "^10.5.0",
|
|
168
168
|
"jsdom": "^29.1.1",
|
|
169
169
|
"micromatch": "^4.0.8",
|
|
170
|
-
"playwright": "^1.
|
|
171
|
-
"prettier": "^3.8.
|
|
172
|
-
"react": "^19.2.
|
|
173
|
-
"react-dom": "^19.2.
|
|
170
|
+
"playwright": "^1.61.0",
|
|
171
|
+
"prettier": "^3.8.4",
|
|
172
|
+
"react": "^19.2.7",
|
|
173
|
+
"react-dom": "^19.2.7",
|
|
174
174
|
"typedoc": "^0.28.19",
|
|
175
175
|
"typescript": "^6.0.3",
|
|
176
|
-
"vitest": "^4.1.
|
|
176
|
+
"vitest": "^4.1.9"
|
|
177
177
|
}
|
|
178
178
|
}
|
package/src/backend/index.ts
CHANGED
|
@@ -10,10 +10,10 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
-
Server,
|
|
14
13
|
type MiddlewareNext,
|
|
15
14
|
type Request,
|
|
16
15
|
type Response,
|
|
16
|
+
Server,
|
|
17
17
|
type Websocket,
|
|
18
18
|
} from '@dacely/hyper-express';
|
|
19
19
|
|
|
@@ -172,7 +172,9 @@ export async function startBackend(options: BackendOptions): Promise<RunningBack
|
|
|
172
172
|
// default upgrade handler (hyper-express links it to the companion ws route). Same-origin and
|
|
173
173
|
// non-browser clients pass; others get 403.
|
|
174
174
|
app.upgrade(wsPath, (request: Request, response: Response) => {
|
|
175
|
-
if (
|
|
175
|
+
if (
|
|
176
|
+
!isWsOriginAllowed(request.headers.origin, request.headers.host, options.allowedOrigins)
|
|
177
|
+
) {
|
|
176
178
|
response.status(403).send();
|
|
177
179
|
return;
|
|
178
180
|
}
|
package/src/cli/create.ts
CHANGED
|
@@ -114,7 +114,7 @@ function scaffold(
|
|
|
114
114
|
'@types/react-dom': '^19.2.3',
|
|
115
115
|
eslint: '^10.2.0',
|
|
116
116
|
prettier: '^3.8.1',
|
|
117
|
-
toilscript: '^0.1.
|
|
117
|
+
toilscript: '^0.1.35',
|
|
118
118
|
typescript: '^6.0.3',
|
|
119
119
|
};
|
|
120
120
|
for (const dep of requiredPackages(features).sort()) {
|
|
@@ -165,10 +165,19 @@ function scaffold(
|
|
|
165
165
|
'.prettierignore':
|
|
166
166
|
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nserver/_emails.ts\nserver/toil-server-env.d.ts\n',
|
|
167
167
|
'.gitignore':
|
|
168
|
-
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\n# Local dev env vars/secrets (never commit)\n.env\n.env.secrets\n',
|
|
169
|
-
// Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled
|
|
168
|
+
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\nhosts/*/_tmpl/\n# Local dev env vars/secrets (never commit)\n.env\n.env.secrets\n',
|
|
169
|
+
// Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled
|
|
170
|
+
// version, and prompt to switch, so the editor loads the toilscript LS plugin wired
|
|
171
|
+
// in server/tsconfig.json (which clears the @database / @data editor false positives).
|
|
170
172
|
'.vscode/settings.json':
|
|
171
|
-
JSON.stringify(
|
|
173
|
+
JSON.stringify(
|
|
174
|
+
{
|
|
175
|
+
'typescript.tsdk': 'node_modules/typescript/lib',
|
|
176
|
+
'typescript.enablePromptUseWorkspaceTsdk': true,
|
|
177
|
+
},
|
|
178
|
+
null,
|
|
179
|
+
4,
|
|
180
|
+
) + '\n',
|
|
172
181
|
'toil-env.d.ts': TOIL_ENV_DTS,
|
|
173
182
|
// Stub typed-routes augmentation (RoutePath = string until the first dev/build regenerates it).
|
|
174
183
|
'toil-routes.d.ts': '// AUTO-GENERATED by toil, do not edit.\nexport {};\n',
|
|
@@ -220,10 +229,16 @@ function scaffold(
|
|
|
220
229
|
null,
|
|
221
230
|
4,
|
|
222
231
|
) + '\n',
|
|
232
|
+
// The toilscript LS plugin teaches the editor about compiler-injected members
|
|
233
|
+
// (`@database` static collections like `Db.users`, the `@data` codec, `@user`), so
|
|
234
|
+
// stock TypeScript stops false-flagging them as TS2339. Editor-only; ignored by tsc.
|
|
223
235
|
'server/tsconfig.json':
|
|
224
236
|
JSON.stringify(
|
|
225
237
|
{
|
|
226
238
|
extends: 'toilscript/std/assembly.json',
|
|
239
|
+
compilerOptions: {
|
|
240
|
+
plugins: [{ name: 'toilscript/std/ts-plugin.cjs' }],
|
|
241
|
+
},
|
|
227
242
|
include: ['./**/*.ts'],
|
|
228
243
|
},
|
|
229
244
|
null,
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -503,6 +503,54 @@ export function checkRestDispatch(f: RestFacts): Check {
|
|
|
503
503
|
};
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Whether the server's tsconfig wires the toilscript language-service plugin. The compiler turns
|
|
508
|
+
* each `@collection` field into a STATIC handle (`GuestbookDb.totals`) and injects the `@data`
|
|
509
|
+
* codec / `@user` members, none of which stock TypeScript can see, so without the plugin the editor
|
|
510
|
+
* false-flags them as TS2339. The plugin (editor-only; never runs under `tsc`) clears them.
|
|
511
|
+
*/
|
|
512
|
+
export function checkServerTsPlugin(present: boolean): Check {
|
|
513
|
+
return present
|
|
514
|
+
? { id: 'server-ts-plugin', label: 'toilscript editor plugin', status: 'pass' }
|
|
515
|
+
: {
|
|
516
|
+
id: 'server-ts-plugin',
|
|
517
|
+
label: 'toilscript editor plugin',
|
|
518
|
+
status: 'warn',
|
|
519
|
+
detail: 'server tsconfig is missing the toilscript LS plugin, so the editor wrongly flags @database static collections (e.g. GuestbookDb.totals) and @data members as TS2339',
|
|
520
|
+
fix: 'Run `toiljs doctor --fix` to add { "plugins": [{ "name": "toilscript/std/ts-plugin.cjs" }] } to your server tsconfig, then pick the workspace TypeScript version and restart the TS server.',
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// --- Security -------------------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
/** Whether the project uses the auth primitive, and whether its session secret is configured. */
|
|
527
|
+
export interface AuthFacts {
|
|
528
|
+
/** A server source references the auth primitive (`AuthService` / `@user` / `@auth`). */
|
|
529
|
+
readonly usesAuth: boolean;
|
|
530
|
+
/** `AUTH_SESSION_SECRET` is assigned a non-empty value in the local secrets source. */
|
|
531
|
+
readonly sessionSecretSet: boolean;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Flags the silent insecure default behind the auth primitive. When a project uses sessions but
|
|
536
|
+
* never sets `AUTH_SESSION_SECRET`, the server falls back to a PUBLISHED dev key (see
|
|
537
|
+
* `server/globals/auth.ts`), so anyone can forge a session cookie and skip login. doctor can only
|
|
538
|
+
* see the local secrets source, so it WARNS (the real secret may live on the deploy target) rather
|
|
539
|
+
* than failing CI on a false positive.
|
|
540
|
+
*/
|
|
541
|
+
export function checkAuthSecrets(f: AuthFacts): Check {
|
|
542
|
+
if (!f.usesAuth || f.sessionSecretSet) {
|
|
543
|
+
return { id: 'auth-secrets', label: 'Session secret', status: 'pass' };
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
id: 'auth-secrets',
|
|
547
|
+
label: 'Session secret',
|
|
548
|
+
status: 'warn',
|
|
549
|
+
detail: 'auth is used but AUTH_SESSION_SECRET is unset: sessions fall back to a PUBLISHED key, so anyone can forge a session cookie and skip login',
|
|
550
|
+
fix: 'Set AUTH_SESSION_SECRET to a long random value in .env.secrets (local) and on your deploy target (also AUTH_OPRF_SEED / AUTH_KEM_SK if you use password login). Generate one: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))".',
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
506
554
|
// --- Summary --------------------------------------------------------------------------------------
|
|
507
555
|
|
|
508
556
|
export function summarize(groups: readonly CheckGroup[]): DoctorSummary {
|
package/src/cli/doctor.ts
CHANGED
|
@@ -10,10 +10,17 @@ import { createRequire } from 'node:module';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
loadConfig,
|
|
15
|
+
type ResolvedToilConfig,
|
|
16
|
+
scanRoutes,
|
|
17
|
+
TOIL_SERVER_ENV_DTS,
|
|
18
|
+
} from 'toiljs/compiler';
|
|
14
19
|
|
|
15
20
|
import {
|
|
21
|
+
type AuthFacts,
|
|
16
22
|
type Check,
|
|
23
|
+
checkAuthSecrets,
|
|
17
24
|
checkBasePath,
|
|
18
25
|
checkConfigLoads,
|
|
19
26
|
checkDevScripts,
|
|
@@ -32,6 +39,7 @@ import {
|
|
|
32
39
|
checkRpcWiring,
|
|
33
40
|
checkSeoUrl,
|
|
34
41
|
checkServerEntry,
|
|
42
|
+
checkServerTsPlugin,
|
|
35
43
|
type CheckStatus,
|
|
36
44
|
checkStyling,
|
|
37
45
|
checkToilconfig,
|
|
@@ -41,8 +49,8 @@ import {
|
|
|
41
49
|
findRelativeAssets,
|
|
42
50
|
hasFailures,
|
|
43
51
|
type RestFacts,
|
|
44
|
-
type RpcFacts,
|
|
45
52
|
RPC_TOILSCRIPT_MIN,
|
|
53
|
+
type RpcFacts,
|
|
46
54
|
satisfiesMin,
|
|
47
55
|
type SourceFile,
|
|
48
56
|
summarize,
|
|
@@ -226,6 +234,61 @@ function gatherRestFacts(root: string, toilconfig: Record<string, unknown> | nul
|
|
|
226
234
|
return { hasControllers, dispatched };
|
|
227
235
|
}
|
|
228
236
|
|
|
237
|
+
/** Whether `.env.secrets` assigns `key` a non-empty value (a bare `KEY=` counts as unset). */
|
|
238
|
+
function secretDefined(root: string, key: string): boolean {
|
|
239
|
+
const raw = readFile(path.join(root, '.env.secrets'));
|
|
240
|
+
if (raw === null) return false;
|
|
241
|
+
return new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=\\s*\\S`, 'm').test(raw);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Whether the server uses the auth primitive (so a missing session secret matters) and whether
|
|
246
|
+
* `AUTH_SESSION_SECRET` is set locally. `getSecure` reads ONLY the `.env.secrets` bucket, so that
|
|
247
|
+
* is the source we check; on a deploy target the secret lives on the dashboard instead.
|
|
248
|
+
*/
|
|
249
|
+
function gatherAuthFacts(root: string, toilconfig: Record<string, unknown> | null): AuthFacts {
|
|
250
|
+
let usesAuth = false;
|
|
251
|
+
for (const src of serverSources(root, toilconfig)) {
|
|
252
|
+
if (/\bAuthService\b/.test(src) || /@user\b/.test(src) || /@auth\b/.test(src)) {
|
|
253
|
+
usesAuth = true;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { usesAuth, sessionSecretSet: secretDefined(root, 'AUTH_SESSION_SECRET') };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The toilscript language-service plugin id wired into a server tsconfig. */
|
|
261
|
+
const TS_PLUGIN_NAME = 'toilscript/std/ts-plugin.cjs';
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The server's tsconfig.json (the one beside a toilconfig entry, conventionally
|
|
265
|
+
* `server/tsconfig.json`), or null if none exists. That is the project the editor uses for server
|
|
266
|
+
* files, so it is where the toilscript LS plugin must live.
|
|
267
|
+
*/
|
|
268
|
+
function serverTsconfigPath(root: string, toilconfig: Record<string, unknown> | null): string | null {
|
|
269
|
+
const dirs = new Set<string>();
|
|
270
|
+
const entries = Array.isArray(toilconfig?.entries)
|
|
271
|
+
? (toilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
|
|
272
|
+
: [];
|
|
273
|
+
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
274
|
+
if (dirs.size === 0) dirs.add(path.join(root, 'server'));
|
|
275
|
+
for (const dir of dirs) {
|
|
276
|
+
const p = path.join(dir, 'tsconfig.json');
|
|
277
|
+
if (fs.existsSync(p)) return p;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Whether a parsed tsconfig's `compilerOptions.plugins` references the toilscript LS plugin. */
|
|
283
|
+
function tsconfigHasToilPlugin(tsconfig: Record<string, unknown> | null): boolean {
|
|
284
|
+
const plugins = asRecord(tsconfig?.compilerOptions)?.plugins;
|
|
285
|
+
if (!Array.isArray(plugins)) return false;
|
|
286
|
+
return plugins.some((p) => {
|
|
287
|
+
const name = asRecord(p)?.name;
|
|
288
|
+
return typeof name === 'string' && name.includes('ts-plugin');
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
229
292
|
interface RpcFixResult {
|
|
230
293
|
/** Files written. */
|
|
231
294
|
readonly changed: string[];
|
|
@@ -353,7 +416,9 @@ function applyRpcFix(root: string): RpcFixResult {
|
|
|
353
416
|
const serverToilconfig = readJsonObject(path.join(root, 'toilconfig.json'));
|
|
354
417
|
if (serverToilconfig !== null) {
|
|
355
418
|
const entries = Array.isArray(serverToilconfig.entries)
|
|
356
|
-
? (serverToilconfig.entries as unknown[]).filter(
|
|
419
|
+
? (serverToilconfig.entries as unknown[]).filter(
|
|
420
|
+
(e): e is string => typeof e === 'string',
|
|
421
|
+
)
|
|
357
422
|
: [];
|
|
358
423
|
const dirs = new Set<string>();
|
|
359
424
|
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
@@ -464,6 +529,64 @@ function applyPrettierFix(root: string, pkg: Record<string, unknown> | null): Rp
|
|
|
464
529
|
return { changed, skipped };
|
|
465
530
|
}
|
|
466
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Wires the editor side of the toilscript server: adds the LS plugin to the server tsconfig (so the
|
|
534
|
+
* editor stops false-flagging `@database` static collections / `@data` members), and points VS Code
|
|
535
|
+
* at the workspace TypeScript (so it actually loads that plugin). Idempotent; only writes real
|
|
536
|
+
* changes, and skips (with a note) configs it can't safely edit (a tsconfig with comments).
|
|
537
|
+
*/
|
|
538
|
+
function applyServerEditorFix(root: string, toilconfig: Record<string, unknown> | null): RpcFixResult {
|
|
539
|
+
const changed: string[] = [];
|
|
540
|
+
const skipped: string[] = [];
|
|
541
|
+
|
|
542
|
+
// 1. The LS plugin in the server tsconfig.
|
|
543
|
+
const tsPath = serverTsconfigPath(root, toilconfig);
|
|
544
|
+
if (tsPath === null) {
|
|
545
|
+
skipped.push('server/tsconfig.json (not found; add the toilscript ts-plugin by hand)');
|
|
546
|
+
} else {
|
|
547
|
+
const rel = path.relative(root, tsPath);
|
|
548
|
+
const raw = readFile(tsPath);
|
|
549
|
+
const parsed = raw !== null ? readJsonObject(tsPath) : null;
|
|
550
|
+
if (parsed === null) {
|
|
551
|
+
skipped.push(`${rel} (JSON with comments; add the "${TS_PLUGIN_NAME}" plugin by hand)`);
|
|
552
|
+
} else if (!tsconfigHasToilPlugin(parsed)) {
|
|
553
|
+
const co = asRecord(parsed.compilerOptions) ?? {};
|
|
554
|
+
const existingPlugins: unknown[] = Array.isArray(co.plugins)
|
|
555
|
+
? (co.plugins as unknown[])
|
|
556
|
+
: [];
|
|
557
|
+
co.plugins = [...existingPlugins, { name: TS_PLUGIN_NAME }];
|
|
558
|
+
parsed.compilerOptions = co;
|
|
559
|
+
writeFile(tsPath, JSON.stringify(parsed, null, 4) + '\n');
|
|
560
|
+
changed.push(rel);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// 2. Make VS Code use the workspace TypeScript, so it loads the plugin above.
|
|
565
|
+
const vsPath = path.join(root, '.vscode', 'settings.json');
|
|
566
|
+
const vsRaw = readFile(vsPath);
|
|
567
|
+
const vs = vsRaw !== null ? readJsonObject(vsPath) : {};
|
|
568
|
+
if (vs === null) {
|
|
569
|
+
skipped.push('.vscode/settings.json (unparseable; set typescript.tsdk by hand)');
|
|
570
|
+
} else {
|
|
571
|
+
let touched = false;
|
|
572
|
+
if (vs['typescript.tsdk'] !== 'node_modules/typescript/lib') {
|
|
573
|
+
vs['typescript.tsdk'] = 'node_modules/typescript/lib';
|
|
574
|
+
touched = true;
|
|
575
|
+
}
|
|
576
|
+
if (vs['typescript.enablePromptUseWorkspaceTsdk'] !== true) {
|
|
577
|
+
vs['typescript.enablePromptUseWorkspaceTsdk'] = true;
|
|
578
|
+
touched = true;
|
|
579
|
+
}
|
|
580
|
+
if (touched) {
|
|
581
|
+
fs.mkdirSync(path.dirname(vsPath), { recursive: true });
|
|
582
|
+
writeFile(vsPath, JSON.stringify(vs, null, 4) + '\n');
|
|
583
|
+
changed.push('.vscode/settings.json');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return { changed, skipped };
|
|
588
|
+
}
|
|
589
|
+
|
|
467
590
|
/** Reads the framework's own package.json (engines + peerDependencies) for the requirements. */
|
|
468
591
|
function frameworkMeta(): { node: string; peers: Record<string, string> } {
|
|
469
592
|
const pkgPath = path.resolve(
|
|
@@ -624,20 +747,37 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
624
747
|
const peerName = (n: string): Check => checkPeer(n, deps[n] ?? null, meta.peers[n] ?? '*');
|
|
625
748
|
const peerChecks = Object.keys(meta.peers).map(peerName);
|
|
626
749
|
|
|
627
|
-
// Server tooling (RPC wiring + the prettier plugin): optionally fix in
|
|
750
|
+
// Server tooling (RPC wiring + the prettier plugin + the editor LS plugin): optionally fix in
|
|
751
|
+
// place, then re-read.
|
|
628
752
|
const rpcFix = serverPresent && opts.fix ? applyRpcFix(root) : null;
|
|
629
753
|
const prettierFix = serverPresent && opts.fix ? applyPrettierFix(root, projectPkg) : null;
|
|
754
|
+
const editorFix = serverPresent && opts.fix ? applyServerEditorFix(root, toilconfig) : null;
|
|
630
755
|
const rpcFacts = gatherRpcFacts(root);
|
|
631
756
|
const restFacts = gatherRestFacts(root, toilconfig);
|
|
757
|
+
const authFacts = gatherAuthFacts(root, toilconfig);
|
|
632
758
|
const prettierPresent = prettierPluginPresent(
|
|
633
759
|
root,
|
|
634
760
|
readJsonObject(path.join(root, 'package.json')),
|
|
635
761
|
);
|
|
762
|
+
// Only assess the LS plugin when a server tsconfig exists and parses; an absent or
|
|
763
|
+
// commented one is left to the user rather than warned on.
|
|
764
|
+
const serverTsPath = serverPresent ? serverTsconfigPath(root, toilconfig) : null;
|
|
765
|
+
const serverTsParsed = serverTsPath ? readJsonObject(serverTsPath) : null;
|
|
766
|
+
const serverTsPluginPresent =
|
|
767
|
+
serverTsPath === null || serverTsParsed === null ? true : tsconfigHasToilPlugin(serverTsParsed);
|
|
636
768
|
const serverFix =
|
|
637
|
-
rpcFix || prettierFix
|
|
769
|
+
rpcFix || prettierFix || editorFix
|
|
638
770
|
? {
|
|
639
|
-
changed: [
|
|
640
|
-
|
|
771
|
+
changed: [
|
|
772
|
+
...(rpcFix?.changed ?? []),
|
|
773
|
+
...(prettierFix?.changed ?? []),
|
|
774
|
+
...(editorFix?.changed ?? []),
|
|
775
|
+
],
|
|
776
|
+
skipped: [
|
|
777
|
+
...(rpcFix?.skipped ?? []),
|
|
778
|
+
...(prettierFix?.skipped ?? []),
|
|
779
|
+
...(editorFix?.skipped ?? []),
|
|
780
|
+
],
|
|
641
781
|
}
|
|
642
782
|
: null;
|
|
643
783
|
|
|
@@ -699,11 +839,17 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
699
839
|
checkRpcWiring(rpcFacts),
|
|
700
840
|
checkRestDispatch(restFacts),
|
|
701
841
|
checkPrettierPlugin(prettierPresent),
|
|
842
|
+
checkServerTsPlugin(serverTsPluginPresent),
|
|
702
843
|
]
|
|
703
844
|
: [checkToilconfig(false)],
|
|
704
845
|
},
|
|
705
846
|
];
|
|
706
847
|
|
|
848
|
+
// Security checks only apply to a server (no server, no sessions to forge).
|
|
849
|
+
if (serverPresent) {
|
|
850
|
+
groups.push({ title: 'Security', checks: [checkAuthSecrets(authFacts)] });
|
|
851
|
+
}
|
|
852
|
+
|
|
707
853
|
const summary = summarize(groups);
|
|
708
854
|
if (opts.json) {
|
|
709
855
|
process.stdout.write(JSON.stringify({ groups, summary, fixed: serverFix }, null, 2) + '\n');
|
|
@@ -724,14 +870,14 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
724
870
|
function renderRpcFix(result: RpcFixResult): void {
|
|
725
871
|
const out: string[] = [];
|
|
726
872
|
if (result.changed.length > 0) {
|
|
727
|
-
out.push(' ' + success('fixed
|
|
873
|
+
out.push(' ' + success('fixed server wiring') + dim(` ${result.changed.join(', ')}`));
|
|
728
874
|
if (result.changed.includes('package.json')) {
|
|
729
875
|
out.push(
|
|
730
876
|
' ' + dim('run your installer (npm/pnpm/yarn) if the toilscript version changed.'),
|
|
731
877
|
);
|
|
732
878
|
}
|
|
733
879
|
} else {
|
|
734
|
-
out.push(' ' + dim('
|
|
880
|
+
out.push(' ' + dim('server wiring already in place, nothing to fix.'));
|
|
735
881
|
}
|
|
736
882
|
for (const item of result.skipped) out.push(' ' + warn('skipped') + dim(` ${item}`));
|
|
737
883
|
process.stdout.write(out.join('\n') + '\n\n');
|
package/src/cli/notify.ts
CHANGED
|
@@ -13,12 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
|
|
14
14
|
import { detectPackageManager } from './update.js';
|
|
15
15
|
import { accent, bold, box, dim, version as cliVersion, warn } from './ui.js';
|
|
16
|
-
import {
|
|
17
|
-
findOutdated,
|
|
18
|
-
isCacheFresh,
|
|
19
|
-
type OutdatedRow,
|
|
20
|
-
parseCheckCache,
|
|
21
|
-
} from './version-check.js';
|
|
16
|
+
import { findOutdated, isCacheFresh, type OutdatedRow, parseCheckCache } from './version-check.js';
|
|
22
17
|
|
|
23
18
|
const REGISTRY_URL = 'https://registry.npmjs.org/toiljs/latest';
|
|
24
19
|
const FETCH_TIMEOUT_MS = 2000;
|
package/src/cli/ui.ts
CHANGED
|
@@ -118,9 +118,7 @@ function visibleWidth(s: string): number {
|
|
|
118
118
|
export function box(lines: readonly string[], paint: (s: string) => string = (s) => s): string {
|
|
119
119
|
const width = lines.reduce((w, l) => Math.max(w, visibleWidth(l)), 0);
|
|
120
120
|
const side = paint('│');
|
|
121
|
-
const body = lines.map(
|
|
122
|
-
(l) => ` ${side} ${l}${' '.repeat(width - visibleWidth(l))} ${side}`,
|
|
123
|
-
);
|
|
121
|
+
const body = lines.map((l) => ` ${side} ${l}${' '.repeat(width - visibleWidth(l))} ${side}`);
|
|
124
122
|
return [
|
|
125
123
|
' ' + paint(`╭${'─'.repeat(width + 4)}╮`),
|
|
126
124
|
...body,
|
|
@@ -167,3 +165,5 @@ export function banner(): void {
|
|
|
167
165
|
const ver = `${dim(' v')}${brand(version())}`;
|
|
168
166
|
process.stdout.write('\n' + lines.join('\n') + '\n\n ' + tagline() + ' ' + ver + '\n\n');
|
|
169
167
|
}
|
|
168
|
+
|
|
169
|
+
|
package/src/cli/version-check.ts
CHANGED
|
@@ -29,7 +29,11 @@ export function parseCheckCache(raw: string): CheckCache | null {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** True when the cached answer is still trustworthy (also stale if the clock went backwards). */
|
|
32
|
-
export function isCacheFresh(
|
|
32
|
+
export function isCacheFresh(
|
|
33
|
+
cache: CheckCache,
|
|
34
|
+
now: number,
|
|
35
|
+
ttlMs: number = CHECK_TTL_MS,
|
|
36
|
+
): boolean {
|
|
33
37
|
return cache.checkedAt <= now && now - cache.checkedAt < ttlMs;
|
|
34
38
|
}
|
|
35
39
|
|