Skip to content

Commit f111056

Browse files
committed
Add worker reference package sync tooling
1 parent 10480cb commit f111056

13 files changed

Lines changed: 545 additions & 11 deletions

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,17 @@ runtime launch later verifies. On Workers, Lantern uses that reviewed identity
219219
to key the Dynamic Worker envelope, so the same approved package bytes can be
220220
reused across sessions without turning reviewed packages into arbitrary
221221
server-side apps. The Worker app path no longer depends on local disk for demo
222-
package import or reviewed runtime delivery. The remaining deployment work is
223-
operational: provision the curated reference package source files into
224-
`PACKAGE_ARTIFACTS` as part of your Cloudflare bootstrap or release flow.
222+
package import or reviewed runtime delivery. Provision the curated reference
223+
package source files into `PACKAGE_ARTIFACTS` during Cloudflare bootstrap or
224+
release with:
225+
226+
```sh
227+
deno task reference:sync --bucket=replace-with-package-artifact-bucket
228+
```
229+
230+
This command shells out to Wrangler's `r2 object put` flow. Authenticate with
231+
Wrangler first or provide a Cloudflare API token in the environment you use for
232+
the command.
225233

226234
For local Worker development, Wrangler now builds a single generated Worker
227235
artifact through `deno task build:worker` before `dev` or `deploy`. The repo
@@ -246,9 +254,11 @@ To run the Worker locally:
246254
- when using `wrangler dev --local`, make sure that local Hyperdrive connection
247255
string includes a password segment because Miniflare rejects passwordless DSNs
248256
even if your local Postgres allows them
249-
- if you want the demo import path to work on Workers, seed
250-
`PACKAGE_ARTIFACTS/reference-packages/<app-id>/source/...` in your local or
251-
remote R2 bucket before calling `/admin/packages/import-reference`
257+
- if you want the demo import path to work on Workers, provision the curated
258+
reference sources into the same bucket first with
259+
`deno task reference:sync --bucket=<bucket-name>` or
260+
`deno task reference:sync --bucket=<bucket-name> --local --persist-to=<dir>`
261+
before calling `/admin/packages/import-reference`
252262
- then run `npx wrangler dev --local`
253263

254264
The reviewed runtime contract remains the same across local Deno preview and the
@@ -344,6 +354,7 @@ Common commands:
344354
- `deno task app:validate <package-root>`
345355
- `deno task app:test-preview <package-root>`
346356
- `deno task app:preview <package-root>`
357+
- `deno task reference:sync --bucket=<bucket-name>`
347358
- `deno task fmt`
348359
- `deno task fmt:check`
349360
- `deno task lint`

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
"app:dev": "deno run --allow-read --allow-net tools/app_dev.ts",
99
"app:test-preview": "deno run --allow-read --allow-net tools/app_test_preview.ts",
1010
"app:new": "deno run --allow-read --allow-write tools/app_new.ts",
11+
"reference:sync": "deno run --allow-read --allow-run tools/reference_sync.ts",
1112
"local:init": "deno run --allow-read --allow-write tools/local_init.ts",
1213
"local:migrate": "deno run --env-file=.env.local --allow-read --allow-env --allow-net src/db/migrate.ts",
1314
"local:seed": "deno run --env-file=.env.local --allow-read --allow-write --allow-env --allow-net tools/local_seed.ts",
1415
"local:bootstrap": "deno task local:seed",
1516
"local:start": "deno run --env-file=.env.local --allow-read --allow-write --allow-env --allow-net main.ts",
1617
"local:dev": "deno run --env-file=.env.local --watch --allow-read --allow-write --allow-env --allow-net main.ts",
1718
"build:worker": "deno bundle --platform=browser src/worker.ts -o output/worker.bundle.mjs",
18-
"check:worker": "deno task build:worker && deno run --allow-read tools/check_worker_bundle.ts",
19+
"check:worker": "deno task build:worker && deno run --allow-read tools/check_worker_bundle.ts && deno run --allow-read tools/check_worker_config.ts",
1920
"fmt": "deno run -A npm:oxfmt@0.43.0 --write .",
2021
"fmt:check": "deno run -A npm:oxfmt@0.43.0 --check .",
2122
"lint": "deno run -A npm:oxlint@1.58.0",

src/app_worker_services.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createHyperdriveDatabasePool, type HyperdriveBinding } from './db/pool.
66
import { createOpsRepository, type OpsRepository } from './ops/repository.ts';
77
import type { EnvReader } from './platform/env.ts';
88
import {
9+
getReferencePackageBucketSourceRoot,
910
importPackage,
1011
isReferencePackageId,
1112
loadPackageSnapshot,
@@ -39,7 +40,7 @@ const WORKER_RUNTIME_ARTIFACT_MESSAGE =
3940
const WORKER_EVIDENCE_ARTIFACT_MESSAGE =
4041
'Cloudflare Workers evidence artifact storage requires an R2 binding named PACKAGE_ARTIFACTS with write access. Bind the reviewed package artifact bucket before accepting anonymous evidence uploads on Workers.';
4142
const WORKER_REFERENCE_PACKAGE_MESSAGE =
42-
'Curated reference packages must be stored in PACKAGE_ARTIFACTS under reference-packages/<app-id>/source before Workers can import or inspect them.';
43+
'Curated reference packages must be stored in PACKAGE_ARTIFACTS under reference-packages/<app-id>/source before Workers can import or inspect them. Run `deno task reference:sync --bucket=<bucket-name>` during Worker bootstrap or release.';
4344
const WORKER_RUNTIME_DELIVERY_MESSAGE =
4445
'Cloudflare Workers reviewed runtime delivery requires a Worker Loader binding named LOADER. Bind the Dynamic Worker loader before serving immutable reviewed runtime bytes from Workers.';
4546
const WORKER_AUTHORING_REFERENCE_MESSAGE =
@@ -161,7 +162,7 @@ function resolveWorkerReferencePackageSource(
161162
const bucket = bindings.PACKAGE_ARTIFACTS;
162163

163164
if (isRuntimeArtifactBucket(bucket) && typeof bucket.list === 'function') {
164-
return createBucketPackageSource(bucket, `reference-packages/${appId}/source`);
165+
return createBucketPackageSource(bucket, getReferencePackageBucketSourceRoot(appId));
165166
}
166167

167168
return createUnsupportedPackageSource(WORKER_REFERENCE_PACKAGE_MESSAGE);

src/app_worker_services_test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assertEquals, assertRejects } from '@std/assert';
22
import { resolveWorkerServices } from './app_worker_services.ts';
33
import { createObjectEnvReader } from './platform/env.ts';
4+
import { getReferencePackageBucketSourceRoot } from './package_review/intake.ts';
45
import { createMemoryPackageSource } from './package_review/package_source.ts';
56
import { verifyReviewedRuntimeContractSignature } from './package_review/runtime_contract.ts';
67
import type { RuntimeArtifactBucket } from './runtime/artifact_store.ts';
@@ -10,7 +11,7 @@ import {
1011
buildRuntimeSessionRecord,
1112
} from './test_helpers/package_review.ts';
1213

13-
const DEMO_BUCKET_SOURCE_ROOT = 'reference-packages/chapter-4-asteroids/source';
14+
const DEMO_BUCKET_SOURCE_ROOT = getReferencePackageBucketSourceRoot('chapter-4-asteroids');
1415
const DEMO_SNAPSHOT_ROOT = 'var/packages/chapter-4-asteroids/0.1.0';
1516
const DEMO_REFERENCE_SOURCE_FILES = {
1617
'manifest.json': `{

src/package_review/intake.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const REFERENCE_PACKAGE_SOURCE_ROOTS = {
1313
'web-checkup': 'examples/apps/web-checkup',
1414
'typescript-ladder-game': 'examples/apps/typescript-ladder-game',
1515
} as const;
16+
export const REFERENCE_PACKAGE_BUCKET_ROOT = 'reference-packages';
1617

1718
export type ReferencePackageId = keyof typeof REFERENCE_PACKAGE_SOURCE_ROOTS;
1819

@@ -57,6 +58,12 @@ export function getReferencePackageSourceRoot(appId: string): string {
5758
return REFERENCE_PACKAGE_SOURCE_ROOTS[appId];
5859
}
5960

61+
export function getReferencePackageBucketSourceRoot(appId: string): string {
62+
assertReferencePackageId(appId);
63+
64+
return joinPath(REFERENCE_PACKAGE_BUCKET_ROOT, appId, 'source');
65+
}
66+
6067
export async function readReferencePackageReviewData(
6168
appId: string,
6269
source: PackageSource,

src/package_review/intake_test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assert, assertEquals, assertRejects, assertThrows } from '@std/assert';
22
import { getTestToolPrivateJwkEnvValue } from '../test_helpers/lti.ts';
33
import {
4+
getReferencePackageBucketSourceRoot,
45
getReferencePackageSourceRoot,
56
importDemoPackage,
67
importPackage,
@@ -214,13 +215,25 @@ Deno.test('curated authoring examples stay explicitly registered in the referenc
214215

215216
assertEquals(ids.includes('template-app'), true);
216217
assertEquals(getReferencePackageSourceRoot('template-app'), 'examples/apps/template');
218+
assertEquals(
219+
getReferencePackageBucketSourceRoot('template-app'),
220+
'reference-packages/template-app/source',
221+
);
217222
assertEquals(ids.includes('web-checkup'), true);
218223
assertEquals(getReferencePackageSourceRoot('web-checkup'), 'examples/apps/web-checkup');
224+
assertEquals(
225+
getReferencePackageBucketSourceRoot('web-checkup'),
226+
'reference-packages/web-checkup/source',
227+
);
219228
assertEquals(ids.includes('typescript-ladder-game'), true);
220229
assertEquals(
221230
getReferencePackageSourceRoot('typescript-ladder-game'),
222231
'examples/apps/typescript-ladder-game',
223232
);
233+
assertEquals(
234+
getReferencePackageBucketSourceRoot('typescript-ladder-game'),
235+
'reference-packages/typescript-ladder-game/source',
236+
);
224237
});
225238

226239
Deno.test('importReferencePackage snapshots web-checkup through the explicit curated registry', async () => {

tools/check_worker_config.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function validateWorkerConfigBindings(configText: string): string[] {
2+
return REQUIRED_BINDINGS.filter(({ pattern }) => !pattern.test(configText)).map(
3+
({ description }) => description,
4+
);
5+
}
6+
7+
const REQUIRED_BINDINGS = [
8+
{
9+
description: 'worker_loaders binding LOADER',
10+
pattern: /"worker_loaders"\s*:\s*\[[\s\S]*?"binding"\s*:\s*"LOADER"/,
11+
},
12+
{
13+
description: 'r2_buckets binding PACKAGE_ARTIFACTS',
14+
pattern: /"r2_buckets"\s*:\s*\[[\s\S]*?"binding"\s*:\s*"PACKAGE_ARTIFACTS"/,
15+
},
16+
] as const;
17+
18+
if (import.meta.main) {
19+
const configPath = new URL('../wrangler.jsonc', import.meta.url);
20+
const configText = await Deno.readTextFile(configPath);
21+
const failures = validateWorkerConfigBindings(configText);
22+
23+
if (failures.length > 0) {
24+
throw new Error(
25+
`wrangler.jsonc is missing required Worker bindings:\n${failures.map((failure) => `- ${failure}`).join('\n')}`,
26+
);
27+
}
28+
29+
console.log('Worker config keeps LOADER and PACKAGE_ARTIFACTS bindings wired.');
30+
}

tools/check_worker_config_test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { assertEquals } from '@std/assert';
2+
import { validateWorkerConfigBindings } from './check_worker_config.ts';
3+
4+
Deno.test('validateWorkerConfigBindings accepts wrangler config with LOADER and PACKAGE_ARTIFACTS', () => {
5+
const config = `{
6+
"worker_loaders": [
7+
{
8+
"binding": "LOADER"
9+
}
10+
],
11+
"r2_buckets": [
12+
{
13+
"binding": "PACKAGE_ARTIFACTS"
14+
}
15+
]
16+
}`;
17+
18+
assertEquals(validateWorkerConfigBindings(config), []);
19+
});
20+
21+
Deno.test('validateWorkerConfigBindings reports missing required Worker bindings', () => {
22+
const config = `{
23+
"worker_loaders": [],
24+
"r2_buckets": []
25+
}`;
26+
27+
assertEquals(validateWorkerConfigBindings(config), [
28+
'worker_loaders binding LOADER',
29+
'r2_buckets binding PACKAGE_ARTIFACTS',
30+
]);
31+
});

tools/local_seed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ if (import.meta.main) {
2727
console.log('Next:');
2828
console.log('1. Run `deno task local:start`.');
2929
console.log('2. Open http://localhost:8417/admin/packages.');
30-
console.log('3. Start with Template App, Web Checkup, or Office Hours Web Lab.');
30+
console.log('3. Start with Template App, Web Checkup, or TypeScript Ladder Game.');
3131
} finally {
3232
await pool.end();
3333
}

tools/reference_sync.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { readArgs, syncReferencePackageSources } from './reference_sync_support.ts';
2+
3+
try {
4+
const args = readArgs(Deno.args);
5+
const summary = await syncReferencePackageSources(args);
6+
7+
console.log(
8+
`${summary.dryRun ? 'Planned' : 'Synced'} ${summary.uploads.length} curated reference source file(s) to ${summary.bucket} (${summary.mode}).`,
9+
);
10+
console.log(`Packages: ${summary.appIds.join(', ')}`);
11+
12+
if (summary.dryRun) {
13+
console.log('Uploads:');
14+
15+
for (const upload of summary.uploads) {
16+
console.log(`- ${summary.bucket}/${upload.objectKey} <- ${upload.sourcePath}`);
17+
}
18+
} else {
19+
console.log('');
20+
console.log('Next:');
21+
console.log('1. Run Workers against the same PACKAGE_ARTIFACTS bucket.');
22+
console.log('2. Import curated apps from /admin/packages/reference.');
23+
}
24+
} catch (error) {
25+
console.error(error instanceof Error ? error.message : 'Reference package source sync failed.');
26+
Deno.exit(1);
27+
}

0 commit comments

Comments
 (0)