@domino-sdk/relay-cli 0.4.0 → 0.5.0

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.
@@ -1,11 +1,18 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { action } from "../runtime.mjs";
3
3
 
4
- const references = ["authoring", "participant-api", "referrals", "troubleshooting"];
4
+ const references = [
5
+ "authoring",
6
+ "participant-api",
7
+ "referrals",
8
+ "troubleshooting",
9
+ ];
5
10
  export function registerDiscoveryCommands(program) {
6
11
  program
7
12
  .command("capabilities")
8
- .description("Discover supported Domino features and their limitations")
13
+ .description(
14
+ "Read this CLI release's bundled capability catalog (not a server probe)",
15
+ )
9
16
  .argument("[id]", "Capability identifier")
10
17
  .action(
11
18
  action(async (_, id) => {
@@ -15,13 +22,25 @@ export function registerDiscoveryCommands(program) {
15
22
  "utf8",
16
23
  ),
17
24
  );
18
- if (!id) return catalog;
25
+ const { version } = JSON.parse(
26
+ await readFile(
27
+ new URL("../../package.json", import.meta.url),
28
+ "utf8",
29
+ ),
30
+ );
31
+ const provenance = {
32
+ kind: "bundled-catalog",
33
+ cliVersion: version,
34
+ serverVerified: false,
35
+ note: "Describes this CLI release. Scope flags do not make it project-specific. Verify the deployed contract before treating missing or conflicting entries as unsupported.",
36
+ };
37
+ if (!id) return { ...catalog, provenance };
19
38
  const capability = catalog.capabilities.find((item) => item.id === id);
20
39
  if (!capability)
21
40
  throw new Error(
22
41
  `Unknown capability. Choose: ${catalog.capabilities.map((item) => item.id).join(", ")}`,
23
42
  );
24
- return capability;
43
+ return { ...capability, provenance };
25
44
  }),
26
45
  );
27
46
  program
@@ -1,4 +1,5 @@
1
- import { writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
2
3
  import { action } from "../runtime.mjs";
3
4
  import { request } from "../connection.mjs";
4
5
  import { initialize, bundleProject } from "../project.mjs";
@@ -76,6 +77,7 @@ async function build({ project, connection, options }, deploy) {
76
77
  return publish(connection, preview.id);
77
78
  }
78
79
  if (options.out) {
80
+ await mkdir(dirname(options.out), { recursive: true });
79
81
  await writeFile(options.out, JSON.stringify(payload, null, 2) + "\n");
80
82
  return {
81
83
  file: options.out,
@@ -20,9 +20,9 @@ export function registerStagingCommands(program) {
20
20
  action(async ({ connection, project, options }) => {
21
21
  if (!project)
22
22
  throw new Error("Run stage inside a hosted campaign checkout.");
23
- if (project.config.app?.kind !== "static")
23
+ if (!project.config.app)
24
24
  throw new Error(
25
- "Cloud staging currently supports static campaign checkouts with app.kind=static in relay.json. This project is not configured for cloud staging. SSR Worker apps use the Cloudflare Worker deployment workflow.",
25
+ "Configure app.kind as static or worker in relay.json before staging.",
26
26
  );
27
27
  const scoped = { ...connection, environment: "test" };
28
28
  const preflight = await request(scoped, "/preflight");
@@ -22,7 +22,7 @@ export async function connectedDev(project, connection, options) {
22
22
  const preflight = await request(connection, "/preflight");
23
23
  if (preflight.development?.status !== "configured")
24
24
  throw new Error(
25
- "Hosted development is not configured. Run domino doctor --remote --json; a Domino operator must enable previews and test sign-in.",
25
+ "Hosted development is not configured. Run domino doctor --remote --json; a Domino operator must enable preview hosting.",
26
26
  );
27
27
  const root = dirname(project.path);
28
28
  const connector = await previewConnector(options.cloudflared);
@@ -126,6 +126,7 @@ export async function connectedDev(project, connection, options) {
126
126
  session = await request(connection, "/development/session", "POST", {
127
127
  ...lease,
128
128
  port,
129
+ routing: project.config.app.routing,
129
130
  });
130
131
  const sync = developmentSync(connection, project.path);
131
132
  await sync();
@@ -143,6 +144,7 @@ export async function connectedDev(project, connection, options) {
143
144
  start("pnpm", ["run", project.config.app.devScript], {
144
145
  DOMINO_APP_PORT: String(port),
145
146
  DOMINO_PREVIEW_ORIGIN: session.url,
147
+ DOMINO_PARTICIPANT_API: connection.apiUrl,
146
148
  });
147
149
  let appReady = false;
148
150
  for (let attempt = 0; attempt < 150 && !stopping; attempt++) {
@@ -190,7 +192,7 @@ export async function connectedDev(project, connection, options) {
190
192
  )
191
193
  );
192
194
  process.stderr.write(
193
- `Preview: ${session.url}\nUses this project's hosted test data and real sign-in. Keep this terminal running. Use domino stage for a preview that stays online.\n`,
195
+ `Preview: ${session.url}\nKeep this terminal running. Use domino stage for a preview that stays online.\n`,
194
196
  );
195
197
  let renewed = Date.now();
196
198
  while (!stopping) {
package/cli/doctor.mjs CHANGED
@@ -5,10 +5,12 @@ import { access, readFile } from "node:fs/promises";
5
5
  import { inspectAgentSkills } from "./agents.mjs";
6
6
  import { request } from "./connection.mjs";
7
7
  import { discoverEntries } from "./discovery.mjs";
8
+ import { packageManager } from "./package-manager.mjs";
8
9
 
9
- export async function diagnose({ project, connection, options }) {
10
+ export async function diagnose({ project, connection, options, scopeSources }) {
10
11
  const checks = [];
11
12
  const root = project ? dirname(project.path) : process.cwd();
13
+ const manager = await packageManager(root);
12
14
  checks.push(
13
15
  project
14
16
  ? { id: "manifest", status: "ok", message: "Project manifest is valid." }
@@ -170,6 +172,13 @@ export async function diagnose({ project, connection, options }) {
170
172
  development: { status: "not-checked" },
171
173
  staging: "not-checked",
172
174
  };
175
+ if (options.remote && !project)
176
+ checks.push({
177
+ id: "remote-access",
178
+ status: "warning",
179
+ message:
180
+ "Remote access was not checked because no relay.json was found. This does not diagnose a failed checkout. Use its checkout error details; do not install skills in the parent directory to repair remote access.",
181
+ });
173
182
  if (options.remote && project) {
174
183
  try {
175
184
  const value = await request(connection, "/access");
@@ -238,6 +247,8 @@ export async function diagnose({ project, connection, options }) {
238
247
  project: connection.project,
239
248
  environment: connection.environment,
240
249
  },
250
+ scopeSources,
251
+ packageManager: manager,
241
252
  agents,
242
253
  readiness,
243
254
  remoteAccess,
@@ -245,6 +256,10 @@ export async function diagnose({ project, connection, options }) {
245
256
  ? "static-app-configured"
246
257
  : "use-existing-app-workflow",
247
258
  checks,
248
- next: "After setup, run the project's checks and verify a real participant action. Setup diagnostics do not prove preview or live readiness.",
259
+ next:
260
+ checks.some((check) => check.id === "sdk" && check.status === "error") &&
261
+ manager.installCommand
262
+ ? `Run ${manager.installCommand} in ${root} and wait for it to finish before rerunning doctor. Then verify a participant action.`
263
+ : "After setup, run the project's checks and verify a real participant action. Setup diagnostics do not prove preview or live readiness.",
249
264
  };
250
265
  }
package/cli/git.mjs CHANGED
@@ -3,6 +3,7 @@ import { access, mkdir, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { apiUrl, request, savedToken } from "./connection.mjs";
6
+ import { packageManager } from "./package-manager.mjs";
6
7
 
7
8
  export function runGit(args, { cwd, input, env = {} } = {}) {
8
9
  return new Promise((resolve, reject) => {
@@ -20,11 +21,13 @@ export function runGit(args, { cwd, input, env = {} } = {}) {
20
21
  stderr += data;
21
22
  });
22
23
  child.on("error", reject);
23
- child.on("close", (code) =>
24
- code === 0
25
- ? resolve(stdout.trim())
26
- : reject(new Error(`Git failed (${code}): ${stderr.trim()}`)),
27
- );
24
+ child.on("close", (code) => {
25
+ if (code === 0) return resolve(stdout.trim());
26
+ const error = new Error(`Git failed (${code}): ${stderr.trim()}`);
27
+ error.exitCode = code;
28
+ error.gitCommand = args[0];
29
+ reject(error);
30
+ });
28
31
  child.stdin.end(input);
29
32
  });
30
33
  }
@@ -86,7 +89,7 @@ export async function requireNewDirectory(directory) {
86
89
  export async function checkout(connection, repository, directory) {
87
90
  if (repository.status !== "ready")
88
91
  throw new Error(
89
- "Repository provisioning is incomplete. Retry domino create.",
92
+ `Repository provisioning is incomplete (status: ${repository.status}). Inspect the existing project in Console, then retry domino checkout. Do not create a replacement project.`,
90
93
  );
91
94
  const remote = new URL(repository.remote);
92
95
  if (
@@ -152,14 +155,42 @@ export async function checkout(connection, repository, directory) {
152
155
  environment: "test",
153
156
  }) + "\n",
154
157
  );
158
+ const manager = await packageManager(root);
155
159
  return {
156
160
  directory: root,
157
161
  remote: repository.remote,
158
- next: "Install dependencies with pnpm install, then run domino dev.",
162
+ packageManager: manager,
163
+ scopeFile: resolve(root, ".git/domino.json"),
164
+ next: manager.installCommand
165
+ ? `In ${root}, run ${manager.installCommand} and wait for success. Then run the project's installed domino doctor --remote --json before domino dev.`
166
+ : "Read package.json and install with the project's declared package manager and lockfile. Wait for success before running the installed domino doctor --remote --json.",
159
167
  };
160
168
  } catch (error) {
161
169
  // This directory was created exclusively by this operation and has no user edits.
162
170
  await rm(root, { recursive: true, force: true });
171
+ if (error.gitCommand) {
172
+ error.code = "CHECKOUT_GIT_FAILED";
173
+ error.details = {
174
+ phase: error.gitCommand,
175
+ exitCode: error.exitCode,
176
+ classification: /repository not found/i.test(error.message)
177
+ ? "repository-unavailable"
178
+ : /authentication failed|401|403/i.test(error.message)
179
+ ? "access-denied"
180
+ : "git-failure",
181
+ repositoryStatus: repository.status,
182
+ organization: repository.organization,
183
+ project: repository.project,
184
+ remote: repository.remote,
185
+ };
186
+ error.message += ` Repository API reported ${repository.status}; Git ${error.gitCommand} failed.`;
187
+ error.message +=
188
+ error.gitCommand === "fetch"
189
+ ? " This does not establish whether provisioning, access, or transport caused the failure. Retry checkout once with the same scope; if it repeats, share these diagnostics with the operator."
190
+ : " Resolve the Git error above before retrying checkout.";
191
+ error.message +=
192
+ " Do not recreate the project or install dependencies in its parent directory.";
193
+ }
163
194
  throw error;
164
195
  }
165
196
  }
@@ -0,0 +1,31 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ // Only generate a runnable command for a validated, exact package-manager pin.
5
+ // Never interpolate arbitrary package.json content into a suggested shell command.
6
+ export async function packageManager(directory) {
7
+ let pkg;
8
+ try {
9
+ pkg = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
10
+ } catch (error) {
11
+ if (error.code === "ENOENT")
12
+ return { declared: null, installCommand: null };
13
+ return {
14
+ declared: null,
15
+ installCommand: null,
16
+ error:
17
+ "Cannot read package.json. Restore valid JSON before installing dependencies.",
18
+ };
19
+ }
20
+ const declared =
21
+ typeof pkg?.packageManager === "string" ? pkg.packageManager : null;
22
+ const pin =
23
+ /^(pnpm|npm)@(\d+\.\d+\.\d+(?:-[\w.-]+)?)(?:\+sha\d+\.[a-fA-F0-9]+)?$/.exec(
24
+ declared ?? "",
25
+ );
26
+ if (!pin) return { declared, installCommand: null };
27
+ const [, name, version] = pin;
28
+ const install = name === "pnpm" ? "install --frozen-lockfile" : "ci";
29
+ const runner = `npx --yes ${name}@${version}`;
30
+ return { declared, installCommand: `${runner} ${install}` };
31
+ }
package/cli/project.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { hostedAppSchema } from "@domino-sdk/relay-cli";
1
2
  import { readFile, writeFile, mkdir, access } from "node:fs/promises";
2
3
  import { dirname, join, resolve } from "node:path";
3
4
  import { z } from "zod";
@@ -41,16 +42,7 @@ export const projectSchema = z
41
42
  })
42
43
  .strict()
43
44
  .optional(),
44
- app: z
45
- .object({
46
- kind: z.literal("static"),
47
- directory: z.string().min(1),
48
- devScript: z.string().min(1),
49
- checkScript: z.string().min(1),
50
- buildScript: z.string().min(1),
51
- })
52
- .strict()
53
- .optional(),
45
+ app: hostedAppSchema.optional(),
54
46
  questTypes: z
55
47
  .array(
56
48
  z
@@ -136,7 +128,14 @@ export async function readJson(path) {
136
128
  }
137
129
 
138
130
  async function loadProject(path) {
139
- const config = projectSchema.parse(await readJson(path));
131
+ const raw = await readJson(path);
132
+ const config = projectSchema.parse(raw);
133
+ const scopeSources = Object.fromEntries(
134
+ ["apiUrl", "organization", "project", "environment"].map((key) => [
135
+ key,
136
+ raw[key] === undefined ? { kind: "default" } : { kind: "manifest", path },
137
+ ]),
138
+ );
140
139
  const dir = dirname(path);
141
140
  // Worktrees keep shared connection metadata in the common Git directory.
142
141
  const gitDir = await runGit(["rev-parse", "--git-common-dir"], {
@@ -154,11 +153,16 @@ async function loadProject(path) {
154
153
  .strict()
155
154
  .parse(await readJson(resolve(dir, gitDir, "domino.json")));
156
155
  Object.assign(config, local);
156
+ for (const key of Object.keys(local))
157
+ scopeSources[key] = {
158
+ kind: "checkout",
159
+ path: resolve(dir, gitDir, "domino.json"),
160
+ };
157
161
  } catch (error) {
158
162
  if (error.code !== "ENOENT") throw error;
159
163
  }
160
164
  }
161
- return { path, config };
165
+ return { path, config, scopeSources };
162
166
  }
163
167
 
164
168
  export async function findProject(path) {
package/cli/runtime.mjs CHANGED
@@ -8,11 +8,18 @@ export function action(handler) {
8
8
  const options = command.optsWithGlobals();
9
9
  const project = await findProject(options.config);
10
10
  const connection = {};
11
+ const scopeSources = {};
11
12
  for (const key of ["apiUrl", "organization", "project", "environment"]) {
12
13
  const env =
13
14
  key === "apiUrl" ? "RELAY_API_URL" : `RELAY_${key.toUpperCase()}`;
14
15
  connection[key] =
15
16
  options[key] ?? process.env[env] ?? project?.config[key];
17
+ scopeSources[key] =
18
+ options[key] !== undefined
19
+ ? { kind: "flag", name: key === "apiUrl" ? "--api-url" : `--${key}` }
20
+ : process.env[env] !== undefined
21
+ ? { kind: "environment", name: env }
22
+ : (project?.scopeSources[key] ?? { kind: "default" });
16
23
  }
17
24
  connection.apiUrl = apiUrl(connection.apiUrl ?? "https://relay.domino.run");
18
25
  if (
@@ -21,7 +28,7 @@ export function action(handler) {
21
28
  )
22
29
  throw new Error("Environment must be test or live.");
23
30
  const result = await handler(
24
- { options, project, connection },
31
+ { options, project, connection, scopeSources },
25
32
  ...command.processedArgs,
26
33
  );
27
34
  console.log(JSON.stringify(result, null, options.json ? undefined : 2));
@@ -42,7 +42,7 @@ Use `domino login` for browser authorization when remote work is needed. As soon
42
42
 
43
43
  Keep the login process running while the user approves in their browser. The CLI polls for approval and saves the credential itself; never ask the user to paste credentials into chat. Wait for the CLI to confirm success before continuing. If the attempt expires, run `domino login` again and send the new code first, then the new link.
44
44
 
45
- Run `domino doctor --remote --json` to verify access and inspect the effective organization, project, API, and environment before remote operations. Flags and RELAY_* environment variables override relay.json.
45
+ Run `domino doctor --remote --json` after dependency installation finishes to verify access before remote operations. Inspect `scope` and `scopeSources`: flags override `RELAY_*` environment variables, then the Git common directory's `domino.json`, then `relay.json`. Checkout writes the untracked local connection file; starter values such as `local` and `campaign` in the manifest may therefore be valid defaults. Credentials live separately in the user config directory. For scope or setup failures, read [troubleshooting](references/troubleshooting.md).
46
46
 
47
47
  Choose the reference that matches the task:
48
48
 
@@ -51,7 +51,7 @@ Choose the reference that matches the task:
51
51
  - Before authoring a quest, read [authoring](references/authoring.md#find-a-snippet) and check the live snippet catalog for a suitable starting point. The same reference covers reusable types, collections, leaderboards, external points, and publication.
52
52
  - For standings, own rank, nearby participants, or pagination, read [participant API](references/participant-api.md#leaderboard-reads).
53
53
 
54
- Before designing referrals, rankings, milestones, or rewards for another participant, run `domino capabilities --json`. Read `domino capabilities <id> --json` for the support boundary. Treat an unlisted capability as unknown and inspect its contract before promising it.
54
+ Before designing referrals, rankings, milestones, or rewards for another participant, run `domino capabilities --json`. Read `domino capabilities <id> --json` for the support boundary. The output's `provenance` identifies a bundled CLI catalog, not a project-specific server probe. Treat an unlisted capability or a disagreement with the deployed API as unknown and inspect its contract before promising it. Check recurrence, daily limits, social verification, and prize fulfillment separately; points or leaderboards alone do not establish those mechanics.
55
55
 
56
56
  For browser integration and submission recovery, read [participant API](references/participant-api.md). For invitation links, qualification, inviter bonuses, or referral milestones, read [referrals](references/referrals.md). For startup, staging, or version failures, read [troubleshooting](references/troubleshooting.md).
57
57
 
@@ -61,4 +61,4 @@ If no CLI is installed, install `@domino-sdk/relay-cli` and `@domino-sdk/relay`
61
61
 
62
62
  Run the app's checks and exercise the requested participant action through the running app. Verify the resulting progress or completion state. Keep fixture-provider results distinct from real external verification.
63
63
 
64
- Return the working preview or local URL, what participant action you verified, and any remaining launch dependency. A successful build, installed skill, or healthy doctor result is not proof of participant behavior. Live publication requires human review in Console; hosted staging uses test data and real sign-in.
64
+ Return the working preview or local URL, what participant action you verified, and any remaining launch dependency. Report failed or blocked browser checks explicitly, including a missing browser executable. A successful build, installed skill, or healthy doctor result is not proof of participant behavior. Live publication requires human review in Console; hosted staging uses test data and real sign-in.
@@ -31,7 +31,7 @@ Source owns reusable quest definitions, collection slots, and deployment default
31
31
 
32
32
  Entry `settings` supplies deployment defaults. Remote custom values take precedence on subsequent deployments. A removed or incompatible setting can block the batch. Use `settingRenames` for a deliberate rename, or have the operator reset the custom value in Console under **Quests**. Changing source defaults does not clear overrides.
33
33
 
34
- Use `domino build --json` or `domino deploy --dry-run --json` for offline bundling. These do not validate effective remote settings. Use `domino deploy --preview --json` to resolve and validate the remote batch without publishing. Check the selected scope before publication.
34
+ Use `domino build --json` or `domino deploy --dry-run --json` for offline bundling. To save the bundle, use `domino build --out .domino-build/deployment.json --json`; the CLI creates missing parent directories. These checks do not validate effective remote settings. Use `domino deploy --preview --json` to resolve and validate the remote batch without publishing. Check the selected scope before publication.
35
35
 
36
36
  Use `domino deploy --environment test --json` when test publication is part of the task. It publishes configured quests, types, collections, and supported interactions together. After a lost publication response, use the returned `domino deploy --resume <id>` recovery command. Starting another deployment may create another immutable release.
37
37
 
@@ -110,3 +110,13 @@ The platform supplies a read-only client bound to the project's configured serve
110
110
  Successful reads, including absent membership, are stored with the account revision, integration revision, server, and observation time. Recovery reuses captured facts. Changing either connection invalidates unfinished reads. After a failed attempt, start a new submission if a connection changed. Membership screening (`pending`) and role requirements remain author decisions.
111
111
 
112
112
  The campaign starter includes participant identity controls. Existing apps use the same APIs through `createParticipantBackend`; see [participant API](participant-api.md#connected-accounts). The managed bot is configured by the platform, never by a quest author. Do not invent message, reaction, or write capabilities: `member` is the only Discord operation currently available. X remains unavailable.
113
+
114
+ The release's `provider` setting selects the photo-check backend used by `ctx.review.photo`; it does not select the evaluator for Discord, quizzes, or other authored checks. `humanReview` allows quest code to return a review decision. It does not automatically route Discord failures to staff. The Discord membership snippet returns accept or reject decisions and leaves provider outages as errors.
115
+
116
+ ## Entry after app-owned pass verification
117
+
118
+ Author the entry quest with `manual({ actor: "staff" })` and no evidence input. The participant app verifies its event pass on its server, resolves the signed-in member, and calls `POST /management/v1/passes/verify` with `{ provider, event, subject, member, quest }`. Use a server-only management key with `confirm` permission in the intended project and environment. Relay records unique pass ownership and submits the entry attempt; it does not decide whether the external ticket is valid. Repeating the same pass for the same member reuses the durable attempt. A different member cannot claim that pass, and one member cannot link a second pass for the same provider and event.
119
+
120
+ ## Hosted server applications
121
+
122
+ Quest discovery and private resource artifacts work the same way for static and Worker apps. Keep quest definitions outside the public asset directory. For server-rendered applications, configure the Worker module output and browser asset output separately in `relay.json`; see [hosted apps](hosted.md). With `app.routing="transparent"`, hosting reserves no paths. Omitting the setting preserves legacy routing. The starter owns its `/relay/*` proxy as ordinary application code; custom handlers can take precedence.
@@ -6,9 +6,9 @@ If Console already created the project and repository, run `domino checkout <pro
6
6
 
7
7
  For a failure after remote project creation, inspect the existing project's state before repeating creation. Keep partially created local directories intact while recovering. Checkout accepts `--directory` for a fresh destination.
8
8
 
9
- Install dependencies from the lockfile. The starter includes pinned SDK, CLI, and local-runtime archives in `.domino`; upgrade them through a platform package release rather than modifying their contents. Read CAMPAIGN.md for the owner's brief.
9
+ Enter the checked-out directory before installing guidance or dependencies. Use checkout's `packageManager.installCommand`, which selects the exact version declared in package.json and preserves the lockfile. For `pnpm@10.34.5`, this is `npx --yes pnpm@10.34.5 install --frozen-lockfile`. Wait for installation to succeed before running doctor; a missing SDK during installation is not a new fault. If guidance is already installed, do not reinstall it as a remedy for checkout or authentication failures. The starter includes pinned SDK, CLI, and local-runtime archives in `.domino`; upgrade them through a platform package release rather than modifying their contents. Read CAMPAIGN.md for the owner's brief.
10
10
 
11
- The starter is a static Vite app using React, TanStack Router file-based routes, and Tailwind CSS v4. Its look is neutral on purpose (greyscale color tokens in `src/styles.css`, no logo or accent) and it ships sample quests and copy that introduce Domino; replace them with the owner's campaign. Its AGENTS.md maps the source and describes the `src/relay` hooks for session, quest state, submission, and recovery. Build the interface on those hooks and keep their recovery behavior.
11
+ The starter uses Vite, React, TanStack Router file-based routes, and Tailwind CSS v4, with an app-owned Worker in `server/index.ts` for participant requests and browser assets. Its look is neutral on purpose (greyscale color tokens in `src/styles.css`, no logo or accent) and it ships sample quests and copy that introduce Domino; replace them with the owner's campaign. Its AGENTS.md maps the source and describes the `src/relay` hooks for session, quest state, submission, and recovery. Build the interface on those hooks and keep their recovery behavior.
12
12
 
13
13
  The starter's relay.json declares discovery patterns for `quests/**/*.quest.ts` and `quests/**/*.quest-type.ts`. Add a matching default-exported file to include it in builds and development sync. Installed snippets use these paths. Use explicit entries only for named exports or per-resource overrides.
14
14
 
@@ -22,7 +22,7 @@ When the task includes sharing a preview, commit the intended changes and push u
22
22
 
23
23
  If stage submission loses its response, repeat the printed commit and request ID. Use `domino builds --json` and `domino logs <build-id> --json` to recover progress after disconnecting. A failed build leaves the previous preview active. Report the URL returned for the successful build rather than constructing one.
24
24
 
25
- Cloud staging supports the starter's static app contract. It does not host arbitrary SSR or customer Worker code. New staging builds use the same project test data as development and real participant sign-in. Existing previews created before this update remain simulated until rebuilt. Test data does not imply that external provider operations are simulated. Hosted app live activation remains a separate platform milestone.
25
+ Cloud staging supports static apps and compiled Cloudflare Worker apps, including SSR and server routes. Configure `app.kind="worker"`, `directory` for private modules, `main` relative to that directory, `compatibilityDate`, optional `compatibilityFlags`, and optional `assetsDirectory` for browser assets. Keep `devScript`, `checkScript`, and `buildScript`. TanStack Start with Cloudflare Vite typically uses `dist/server`, `index.js`, `dist/client`, and `nodejs_compat`. The dev server must honor `DOMINO_APP_PORT` and `DOMINO_PREVIEW_ORIGIN`. The build must also emit the starter's private `.domino-build` quest/catalog artifacts. Set `app.routing="transparent"` to preserve methods, cookies, and app authorization. Hosting reserves no URL paths and forwards requests and responses unchanged. The starter explicitly proxies `/relay/*` in its own server, used in both development and staging; apps can override or remove that handler. Use `proxyParticipant(request, { baseUrl, fetch, hosted: true })` to integrate Domino-managed sign-in explicitly. The API validates the registered app origin, session, and CSRF headers. `DOMINO_PARTICIPANT_API` supplies the selected upstream during connected development. `runWorkerFirst: true` lets your Worker handle even requests matching browser assets through its ASSETS binding. Non-secret string `vars` are supported; customer secrets and resource bindings are not provisioned by this contract. New staging builds use the same project test data as development and real participant sign-in. Omitted routing or `app.routing="legacy"` preserves existing gateway behavior, including implicit `/relay/` routes. To migrate, add the app-owned proxy, explicitly select transparent routing, restart and test the preview with the updated CLI, then commit and stage. Routing is recorded per build and preview session; old records and old CLI requests remain legacy. Test data does not imply that external provider operations are simulated. Hosted app live activation remains a separate platform milestone.
26
26
 
27
27
  For referral programs, register authored modules in `referrals`. `domino check` and `domino dev` include them even without quests. Preserve invitation codes across the app's sign-in redirect and verify acceptance through the configured participant proxy before any quest completes. See [referrals](referrals.md).
28
28
 
@@ -1,6 +1,6 @@
1
1
  # Participant API
2
2
 
3
- Create `createBrowserRelayClient({ baseUrl: "/relay" })` from `@domino-sdk/relay/browser`. The browser client uses cookies and durable submission recovery. The app's server must route `/relay` through the supported participant proxy. Management tokens are not participant credentials.
3
+ Create `createBrowserRelayClient({ baseUrl: "/relay" })` from `@domino-sdk/relay/browser`. The browser client uses cookies and durable submission recovery. With `app.routing="transparent"`, the app explicitly mounts its participant integration at `/relay/*`; hosting does not intercept or reserve that prefix. Existing apps without that setting retain the gateway participant proxy. Management tokens are not participant credentials.
4
4
 
5
5
  ## Sign in
6
6
 
@@ -40,7 +40,7 @@ The browser still uses `createBrowserRelayClient({ baseUrl: "/relay" })`. Reques
40
40
 
41
41
  The adapter uses `POST /v1/auth/exchange` with `{ subject, previousToken? }` and the integration key as a server bearer credential. Scope and the member role come from the registered integration, never from request input. It adds a server-only integration header when proxying participant requests. A plain exchange result is not a substitute for configuring that routing; use the adapter.
42
42
 
43
- `https://console.domino.run/relay` proxies management requests only. Use the participant API origin for the adapter. The lower-level `proxyParticipant` remains available for deployments with preconfigured participant routing; it deliberately excludes browser Authorization and organization/project/environment selectors.
43
+ `https://console.domino.run/relay` proxies management requests only. Use the participant API origin for the adapter. For Domino-managed sign-in in a hosted preview or staging app, explicitly call `proxyParticipant(request, { baseUrl: "https://relay.domino.run", fetch, hosted: true })` from an app-owned route. The API resolves the app origin against its registration and still validates sessions and CSRF. The lower-level `proxyParticipant` also supports deployments with preconfigured participant routing; it deliberately excludes browser Authorization and organization/project/environment selectors.
44
44
 
45
45
  ## Display and complete quests
46
46
 
@@ -107,3 +107,11 @@ To choose a different identity, first call `relay.auth.disconnect("discord")`. T
107
107
  The campaign starter includes these account controls. Apps using `createParticipantBackend` retain their own login and use the same APIs. The adapter preserves OAuth binding cookies and its session during return navigation; the application's own login cookie must also support top-level OAuth returns. If the app session changes during consent, start connection again.
108
108
 
109
109
  After changing an account or integration during a failed attempt, use the quest controller's `discard()` to clear the saved failed submission, then submit again. The starter labels this **Start a new submission**. Discard is allowed only after a confirmed failed or rejected attempt. It keeps server history and cannot discard an active or accepted attempt. Use `resume()` when the connection has not changed to preserve captured facts.
110
+
111
+ ## Resource IDs
112
+
113
+ Treat resource IDs as opaque strings and store the complete value. New generated IDs keep their resource prefix, such as `member_`, `session_`, or `attempt_`, followed by 22 random alphanumeric characters. Existing IDs remain valid. Do not parse the suffix as a UUID.
114
+
115
+ ## Removed demo shortcuts
116
+
117
+ `auth.demoSignIn` and the campaign template endpoint are no longer supported. Use managed sign-in or a registered identity integration. The platform participant `me.linkPass` route remains unconfigured. An app can handle that route itself and verify a pass on its server, then call `POST /management/v1/passes/verify` with a server-only management key carrying `confirm` permission. Send `{ provider, event, subject, member, quest }` and the usual organization, project, and environment headers. Resolve the member from the authenticated participant session, never from browser input. The quest must be staff-confirmed with no evidence. Relay binds ownership uniquely per provider/event in that project, preserves the authenticated management actor, and retries the same entry attempt. There is no built-in event-code verifier.
@@ -1,5 +1,23 @@
1
1
  # Development and staging diagnostics
2
2
 
3
+ ## Scope and installation
4
+
5
+ Read `scopeSources` next to `scope` in `domino doctor --json`. Each value comes from a command flag, a named `RELAY_*` environment variable, the checkout's local connection file, the manifest, or a default. Precedence is flags > environment > checkout connection > manifest. `domino checkout` writes `.git/domino.json`; linked Git worktrees read the file in their shared Git common directory. This untracked file contains API, organization, project, and environment identifiers, not credentials. Do not rewrite starter manifest values merely because they differ from the effective scope.
6
+
7
+ Use the `packageManager.installCommand` printed by checkout or doctor. It selects the declared version with a frozen lockfile. Wait for the install process to finish before diagnosing missing dependencies. Run checks from the project directory. Without a manifest, `doctor --remote` does not probe the remote and cannot explain why checkout failed.
8
+
9
+ For `CHECKOUT_GIT_FAILED`, retain the original Git message and the structured `details`: failed phase, exit code, error classification, repository status reported by the API, project, and remote. A `ready` API status combined with a Git "Repository not found" error does not establish whether provisioning, permissions, or transport caused the failure. Retry the same checkout once. If it fails again, send the diagnostics to the operator and keep the existing Console project. Do not create a replacement, reinstall skills in a parent directory, or repeatedly run unrelated diagnostics.
10
+
11
+ ## Capability disagreements
12
+
13
+ `domino capabilities` reads the catalog bundled with the CLI version in `provenance.cliVersion`. It does not request server capabilities; `serverVerified` is false even when project flags are supplied. Compare the installed version, the applicable public contract, and the actual deployed endpoint before changing the architecture. A successful management route does not by itself prove participant access or payout support. A missing route or catalog entry does not prove platform-wide non-support.
14
+
15
+ ## Verification failures
16
+
17
+ Keep build results, browser rendering, and participant behavior separate. If a browser executable is missing or a check fails, report the exact blocked check and leave its behavior unverified. Do not turn a generated visual sample or a successful build into a claim that the app works in a browser.
18
+
19
+ ## Preview diagnostics
20
+
3
21
  Start with `domino doctor --json`. Add `--remote` to verify project access, publication permission, hosting configuration and repository readiness. Configuration checks do not prove a successful build or real participant sign-in. If cloud hosting is missing, a Domino operator must configure it; changing quest code will not fix it.
4
22
 
5
23
  `domino check` runs the app's checks and bundles authored entries. `domino deploy --preview --environment test --json` validates the effective deployment against the server without publishing. Test and live data are separate; test does not imply external provider requests are fake.
package/cli.mjs CHANGED
@@ -82,7 +82,8 @@ const program = new Command()
82
82
  .addHelpText(
83
83
  "after",
84
84
  `
85
- Scope: flags > RELAY_* environment variables > nearest relay.json.
85
+ Scope: flags > RELAY_* environment variables > Git-common-directory domino.json > nearest relay.json.
86
+ Run domino doctor --json to see effective scope values and their sources.
86
87
  API default: https://relay.domino.run. Init defaults to test.
87
88
  Credentials: RELAY_MANAGEMENT_TOKEN > saved token for the API origin.
88
89
  Run domino login for browser sign-in. Live publication requires Console.
@@ -108,6 +109,8 @@ try {
108
109
  ? JSON.stringify({
109
110
  error: error.message,
110
111
  ...(error.status ? { status: error.status } : {}),
112
+ ...(error.code ? { code: error.code } : {}),
113
+ ...(error.details ? { details: error.details } : {}),
111
114
  })
112
115
  : `Error: ${error.message}`,
113
116
  );
package/dist/index.d.ts CHANGED
@@ -1,6 +1,37 @@
1
1
  import { pointsMutationSchema, referralControlSchema, leaderboardQuerySchema, leaderboardExclusionSchema, leaderboardGroupSchema, projectDeploymentSchema, publishDeploymentSchema, catalogDeploymentSchema, saveQuestDraftSchema, publishQuestDraftSchema, collectionBeginSchema, configurePickupSchema } from '@domino-sdk/relay';
2
2
  import { z } from 'zod';
3
3
 
4
+ declare const appRoutingSchema: z.ZodDefault<z.ZodEnum<{
5
+ legacy: "legacy";
6
+ transparent: "transparent";
7
+ }>>;
8
+ declare const hostedAppSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
9
+ routing: z.ZodDefault<z.ZodEnum<{
10
+ legacy: "legacy";
11
+ transparent: "transparent";
12
+ }>>;
13
+ devScript: z.ZodString;
14
+ checkScript: z.ZodString;
15
+ buildScript: z.ZodString;
16
+ kind: z.ZodLiteral<"static">;
17
+ directory: z.ZodString;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ runWorkerFirst: z.ZodDefault<z.ZodBoolean>;
20
+ main: z.ZodString;
21
+ compatibilityDate: z.ZodString;
22
+ compatibilityFlags: z.ZodDefault<z.ZodArray<z.ZodString>>;
23
+ vars: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
24
+ routing: z.ZodDefault<z.ZodEnum<{
25
+ legacy: "legacy";
26
+ transparent: "transparent";
27
+ }>>;
28
+ devScript: z.ZodString;
29
+ checkScript: z.ZodString;
30
+ buildScript: z.ZodString;
31
+ kind: z.ZodLiteral<"worker">;
32
+ directory: z.ZodString;
33
+ assetsDirectory: z.ZodOptional<z.ZodString>;
34
+ }, z.core.$strict>], "kind">;
4
35
  declare const repositorySchema: z.ZodObject<{
5
36
  id: z.ZodString;
6
37
  organization: z.ZodString;
@@ -32,6 +63,11 @@ declare const stageRequestSchema: z.ZodObject<{
32
63
  requestId: z.ZodString;
33
64
  }, z.core.$strict>;
34
65
  declare const buildIdentitySchema: z.ZodObject<{
66
+ routing: z.ZodDefault<z.ZodEnum<{
67
+ legacy: "legacy";
68
+ transparent: "transparent";
69
+ }>>;
70
+ slug: z.ZodOptional<z.ZodString>;
35
71
  runtime: z.ZodDefault<z.ZodEnum<{
36
72
  simulation: "simulation";
37
73
  "hosted-test": "hosted-test";
@@ -82,6 +118,11 @@ declare const buildStatusSchema: z.ZodObject<{
82
118
  }, z.core.$strip>;
83
119
  type BuildStatus = z.infer<typeof buildStatusSchema>;
84
120
  declare const hostedBuildSchema: z.ZodObject<{
121
+ routing: z.ZodDefault<z.ZodEnum<{
122
+ legacy: "legacy";
123
+ transparent: "transparent";
124
+ }>>;
125
+ slug: z.ZodOptional<z.ZodString>;
85
126
  runtime: z.ZodDefault<z.ZodEnum<{
86
127
  simulation: "simulation";
87
128
  "hosted-test": "hosted-test";
@@ -123,6 +164,11 @@ declare const hostedBuildSchema: z.ZodObject<{
123
164
  type HostedBuild = z.infer<typeof hostedBuildSchema>;
124
165
  declare const buildSnapshotSchema: z.ZodObject<{
125
166
  builds: z.ZodArray<z.ZodObject<{
167
+ routing: z.ZodDefault<z.ZodEnum<{
168
+ legacy: "legacy";
169
+ transparent: "transparent";
170
+ }>>;
171
+ slug: z.ZodOptional<z.ZodString>;
126
172
  runtime: z.ZodDefault<z.ZodEnum<{
127
173
  simulation: "simulation";
128
174
  "hosted-test": "hosted-test";
@@ -162,6 +208,11 @@ declare const buildSnapshotSchema: z.ZodObject<{
162
208
  message: z.ZodString;
163
209
  }, z.core.$strip>>;
164
210
  current: z.ZodNullable<z.ZodObject<{
211
+ routing: z.ZodDefault<z.ZodEnum<{
212
+ legacy: "legacy";
213
+ transparent: "transparent";
214
+ }>>;
215
+ slug: z.ZodOptional<z.ZodString>;
165
216
  runtime: z.ZodDefault<z.ZodEnum<{
166
217
  simulation: "simulation";
167
218
  "hosted-test": "hosted-test";
@@ -201,6 +252,11 @@ declare const buildSnapshotSchema: z.ZodObject<{
201
252
  message: z.ZodString;
202
253
  }, z.core.$strip>>;
203
254
  selected: z.ZodNullable<z.ZodObject<{
255
+ routing: z.ZodDefault<z.ZodEnum<{
256
+ legacy: "legacy";
257
+ transparent: "transparent";
258
+ }>>;
259
+ slug: z.ZodOptional<z.ZodString>;
204
260
  runtime: z.ZodDefault<z.ZodEnum<{
205
261
  simulation: "simulation";
206
262
  "hosted-test": "hosted-test";
@@ -245,6 +301,27 @@ type BuildSnapshot = z.infer<typeof buildSnapshotSchema>;
245
301
  declare const assetPathSchema: z.ZodString;
246
302
  declare const buildArtifactsSchema: z.ZodObject<{
247
303
  format: z.ZodLiteral<1>;
304
+ routing: z.ZodDefault<z.ZodEnum<{
305
+ legacy: "legacy";
306
+ transparent: "transparent";
307
+ }>>;
308
+ worker: z.ZodOptional<z.ZodObject<{
309
+ modules: z.ZodArray<z.ZodObject<{
310
+ path: z.ZodString;
311
+ type: z.ZodEnum<{
312
+ "application/javascript+module": "application/javascript+module";
313
+ "application/wasm": "application/wasm";
314
+ "text/plain": "text/plain";
315
+ "application/octet-stream": "application/octet-stream";
316
+ }>;
317
+ content: z.ZodString;
318
+ }, z.core.$strict>>;
319
+ runWorkerFirst: z.ZodDefault<z.ZodBoolean>;
320
+ main: z.ZodString;
321
+ compatibilityDate: z.ZodString;
322
+ compatibilityFlags: z.ZodDefault<z.ZodArray<z.ZodString>>;
323
+ vars: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
324
+ }, z.core.$strict>>;
248
325
  assets: z.ZodArray<z.ZodObject<{
249
326
  path: z.ZodString;
250
327
  content: z.ZodString;
@@ -319,6 +396,7 @@ declare const projectSetupSchema: z.ZodObject<{
319
396
  brief: z.ZodString;
320
397
  }, z.core.$strict>;
321
398
  declare const projectSchema: z.ZodObject<{
399
+ slug: z.ZodString;
322
400
  setup: z.ZodOptional<z.ZodObject<{
323
401
  mode: z.ZodEnum<{
324
402
  new: "new";
@@ -378,6 +456,7 @@ declare const accessSchema: z.ZodObject<{
378
456
  admin: z.ZodBoolean;
379
457
  local: z.ZodBoolean;
380
458
  projects: z.ZodArray<z.ZodObject<{
459
+ slug: z.ZodString;
381
460
  setup: z.ZodOptional<z.ZodObject<{
382
461
  mode: z.ZodEnum<{
383
462
  new: "new";
@@ -5615,6 +5694,7 @@ declare function createManagementClient(options: {
5615
5694
  admin: boolean;
5616
5695
  local: boolean;
5617
5696
  projects: {
5697
+ slug: string;
5618
5698
  id: string;
5619
5699
  organization: string;
5620
5700
  project: string;
@@ -5631,6 +5711,7 @@ declare function createManagementClient(options: {
5631
5711
  }[];
5632
5712
  }>;
5633
5713
  createProject: (input: z.infer<typeof projectInputSchema>) => Promise<{
5714
+ slug: string;
5634
5715
  id: string;
5635
5716
  organization: string;
5636
5717
  project: string;
@@ -6781,4 +6862,4 @@ declare function createManagementClient(options: {
6781
6862
  }>;
6782
6863
  };
6783
6864
 
6784
- export { type Access, type AuditRecord, type BuildArtifacts, type BuildIdentity, type BuildSnapshot, type BuildStatus, type HostedBuild, type ListQuery, type ManagementPage, type Permission, type Project, accessSchema, accountProfileSchema, assetPathSchema, auditRecordSchema, buildArtifactsSchema, buildIdentitySchema, buildPhaseSchema, buildSnapshotSchema, buildStatusSchema, commitSchema, createManagementClient, createRepositorySchema, developmentPreflightSchema, deviceApproveSchema, deviceRedeemSchema, deviceStartSchema, environmentSchema, grantSchema, hostedBuildSchema, inventorySchema, memberAccountSchema, memberDetailSchema, memberSchema, metricsSchema, operationInputSchema, operationSchema, pageSchema, permissionSchema, projectInputSchema, projectSchema, projectSetupSchema, querySchema, readinessSchema, repositorySchema, rolePermissions, roleSchema, stageRequestSchema, stockInputSchema, tokenInputSchema, tokenSchema };
6865
+ export { type Access, type AuditRecord, type BuildArtifacts, type BuildIdentity, type BuildSnapshot, type BuildStatus, type HostedBuild, type ListQuery, type ManagementPage, type Permission, type Project, accessSchema, accountProfileSchema, appRoutingSchema, assetPathSchema, auditRecordSchema, buildArtifactsSchema, buildIdentitySchema, buildPhaseSchema, buildSnapshotSchema, buildStatusSchema, commitSchema, createManagementClient, createRepositorySchema, developmentPreflightSchema, deviceApproveSchema, deviceRedeemSchema, deviceStartSchema, environmentSchema, grantSchema, hostedAppSchema, hostedBuildSchema, inventorySchema, memberAccountSchema, memberDetailSchema, memberSchema, metricsSchema, operationInputSchema, operationSchema, pageSchema, permissionSchema, projectInputSchema, projectSchema, projectSetupSchema, querySchema, readinessSchema, repositorySchema, rolePermissions, roleSchema, stageRequestSchema, stockInputSchema, tokenInputSchema, tokenSchema };
package/dist/index.js CHANGED
@@ -32,6 +32,46 @@ import { z as z3 } from "zod";
32
32
  // src/hosting.ts
33
33
  import { z } from "zod";
34
34
  import { identifier } from "@domino-sdk/relay";
35
+ var outputPathSchema = z.string().min(1).max(512).refine(
36
+ (path) => path.split("/").every(
37
+ (part) => /^[\w-][\w.-]*$/.test(part) && part !== "node_modules"
38
+ ),
39
+ "Expected a relative build output path without hidden segments"
40
+ );
41
+ var appRoutingSchema = z.enum(["legacy", "transparent"]).default("legacy");
42
+ var appScripts = {
43
+ routing: appRoutingSchema,
44
+ devScript: z.string().regex(/^[\w:-]+$/),
45
+ checkScript: z.string().regex(/^[\w:-]+$/),
46
+ buildScript: z.string().regex(/^[\w:-]+$/)
47
+ };
48
+ var workerSettings = {
49
+ runWorkerFirst: z.boolean().default(false),
50
+ main: outputPathSchema.refine(
51
+ (path) => /\.m?js$/.test(path),
52
+ "Expected a compiled JavaScript entry"
53
+ ),
54
+ compatibilityDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
55
+ compatibilityFlags: z.array(z.string().min(1)).max(50).default([]),
56
+ vars: z.record(
57
+ z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).refine((name) => name !== "ASSETS"),
58
+ z.string()
59
+ ).default({})
60
+ };
61
+ var hostedAppSchema = z.discriminatedUnion("kind", [
62
+ z.object({
63
+ kind: z.literal("static"),
64
+ directory: outputPathSchema,
65
+ ...appScripts
66
+ }).strict(),
67
+ z.object({
68
+ kind: z.literal("worker"),
69
+ directory: outputPathSchema,
70
+ assetsDirectory: outputPathSchema.optional(),
71
+ ...appScripts,
72
+ ...workerSettings
73
+ }).strict()
74
+ ]);
35
75
  var repositorySchema = z.object({
36
76
  id: z.string().uuid(),
37
77
  organization: identifier,
@@ -60,6 +100,8 @@ var stageRequestSchema = z.object({
60
100
  requestId: z.string().uuid()
61
101
  }).strict();
62
102
  var buildIdentitySchema = z.object({
103
+ routing: appRoutingSchema,
104
+ slug: z.string().min(1).max(35).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
63
105
  runtime: z.enum(["simulation", "hosted-test"]).default("simulation"),
64
106
  id: z.string().uuid(),
65
107
  repository: z.string().uuid(),
@@ -99,11 +141,29 @@ var buildSnapshotSchema = z.object({
99
141
  logs: z.string()
100
142
  });
101
143
  var assetPathSchema = z.string().max(512).refine(
102
- (path) => path.startsWith("/") && !/[\\\u0000-\u001f?#%]/.test(path) && path.slice(1).split("/").every((part) => part.length > 0 && !part.startsWith(".")) && !path.startsWith("/relay/") && !path.startsWith("/__domino/"),
103
- "Expected a public asset path without hidden or reserved segments"
144
+ (path) => path.startsWith("/") && !/[\\\u0000-\u001f?#%]/.test(path) && path.slice(1).split("/").every((part) => part.length > 0 && !part.startsWith(".")),
145
+ "Expected a public asset path without hidden segments"
104
146
  );
105
147
  var buildArtifactsSchema = z.object({
106
148
  format: z.literal(1),
149
+ routing: appRoutingSchema,
150
+ worker: z.object({
151
+ ...workerSettings,
152
+ modules: z.array(
153
+ z.object({
154
+ path: outputPathSchema.refine((path) => path !== "metadata"),
155
+ type: z.enum([
156
+ "application/javascript+module",
157
+ "application/wasm",
158
+ "text/plain",
159
+ "application/octet-stream"
160
+ ]),
161
+ content: z.string().max(14e6).regex(
162
+ /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
163
+ )
164
+ }).strict()
165
+ ).min(1).max(500)
166
+ }).strict().optional(),
107
167
  assets: z.array(
108
168
  z.object({
109
169
  path: assetPathSchema,
@@ -112,13 +172,36 @@ var buildArtifactsSchema = z.object({
112
172
  ),
113
173
  size: z.number().int().min(0).max(1e7)
114
174
  }).strict()
115
- ).min(1).max(500),
175
+ ).max(500),
116
176
  quests: z.unknown(),
117
177
  catalog: z.unknown()
118
178
  }).strict().superRefine((value, ctx) => {
179
+ if (value.worker) {
180
+ const modules = value.worker.modules;
181
+ if (new Set(modules.map((m) => m.path)).size !== modules.length)
182
+ ctx.addIssue({
183
+ code: "custom",
184
+ message: "Duplicate Worker module paths"
185
+ });
186
+ if (!modules.some(
187
+ (m) => m.path === value.worker?.main && m.type === "application/javascript+module"
188
+ ))
189
+ ctx.addIssue({
190
+ code: "custom",
191
+ message: "Worker entry module is missing"
192
+ });
193
+ if (modules.reduce(
194
+ (sum, m) => sum + m.content.length * 3 / 4 - (m.content.endsWith("==") ? 2 : m.content.endsWith("=") ? 1 : 0),
195
+ 0
196
+ ) > 1e7)
197
+ ctx.addIssue({
198
+ code: "custom",
199
+ message: "Worker modules exceed 10 MB"
200
+ });
201
+ }
119
202
  if (new Set(value.assets.map((a) => a.path)).size !== value.assets.length)
120
203
  ctx.addIssue({ code: "custom", message: "Duplicate asset paths" });
121
- if (!value.assets.some((a) => a.path === "/index.html"))
204
+ if (!value.worker && !value.assets.some((a) => a.path === "/index.html"))
122
205
  ctx.addIssue({
123
206
  code: "custom",
124
207
  message: "Build must include index.html"
@@ -193,6 +276,7 @@ var projectSetupSchema = z3.object({
193
276
  brief: z3.string().trim().min(1).max(4e3)
194
277
  }).strict();
195
278
  var projectSchema = z3.object({
279
+ slug: z3.string().min(1).max(35).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
196
280
  setup: projectSetupSchema.optional(),
197
281
  id: z3.string().min(1),
198
282
  organization: identifier2,
@@ -526,6 +610,7 @@ function createManagementClient(options) {
526
610
  export {
527
611
  accessSchema,
528
612
  accountProfileSchema,
613
+ appRoutingSchema,
529
614
  assetPathSchema,
530
615
  auditRecordSchema,
531
616
  buildArtifactsSchema,
@@ -542,6 +627,7 @@ export {
542
627
  deviceStartSchema,
543
628
  environmentSchema,
544
629
  grantSchema,
630
+ hostedAppSchema,
545
631
  hostedBuildSchema,
546
632
  inventorySchema,
547
633
  memberAccountSchema,
package/package.json CHANGED
@@ -10,9 +10,9 @@
10
10
  "dependencies": {
11
11
  "commander": "15.0.0",
12
12
  "zod": "4.3.6",
13
- "@domino-sdk/relay": "0.4.0"
13
+ "@domino-sdk/relay": "0.5.0"
14
14
  },
15
- "version": "0.4.0",
15
+ "version": "0.5.0",
16
16
  "bin": {
17
17
  "domino": "./cli.mjs"
18
18
  },