@domino-sdk/relay-runtime 0.1.0 → 0.2.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.
package/dist/index.js CHANGED
@@ -15283,7 +15283,7 @@ function createRelayClient(options) {
15283
15283
  }
15284
15284
  const submission = submissions(
15285
15285
  {
15286
- session: () => request("/v1/auth/session", sessionSchema.nullable()),
15286
+ session: () => request("/v1/auth/session", sessionSchema2.nullable()),
15287
15287
  upload: (file2, action, session) => request("/v1/me/evidence", evidenceSchema, {
15288
15288
  method: "POST",
15289
15289
  headers: {
@@ -15334,7 +15334,7 @@ function createRelayClient(options) {
15334
15334
  throw new Error("There is no failed attempt to retry.");
15335
15335
  const session = await request(
15336
15336
  "/v1/auth/session",
15337
- sessionSchema.nullable()
15337
+ sessionSchema2.nullable()
15338
15338
  );
15339
15339
  if (session?.principal.kind !== "member")
15340
15340
  throw new Error("Sign in to retry verification.");
@@ -15358,7 +15358,7 @@ function createRelayClient(options) {
15358
15358
  observe: (listener, options2) => {
15359
15359
  const observer = observeProgress(
15360
15360
  {
15361
- session: (signal2) => request("/v1/auth/session", sessionSchema.nullable(), { signal: signal2 }),
15361
+ session: (signal2) => request("/v1/auth/session", sessionSchema2.nullable(), { signal: signal2 }),
15362
15362
  progress: (session, signal2) => request("/v1/me/experience", questExperienceSchema, {
15363
15363
  signal: signal2,
15364
15364
  headers: { "X-Relay-Session": session }
@@ -15418,7 +15418,7 @@ function createRelayClient(options) {
15418
15418
  emailVerify: (challenge, code) => changeSession(
15419
15419
  () => request(
15420
15420
  "/v1/auth/email/verify",
15421
- sessionSchema,
15421
+ sessionSchema2,
15422
15422
  json3({ challenge, code })
15423
15423
  )
15424
15424
  ),
@@ -15432,9 +15432,9 @@ function createRelayClient(options) {
15432
15432
  })
15433
15433
  )
15434
15434
  ),
15435
- session: () => request("/v1/auth/session", sessionSchema.nullable()),
15435
+ session: () => request("/v1/auth/session", sessionSchema2.nullable()),
15436
15436
  demoSignIn: (code) => changeSession(
15437
- () => request("/v1/auth/demo", sessionSchema, json3({ code }))
15437
+ () => request("/v1/auth/demo", sessionSchema2, json3({ code }))
15438
15438
  ),
15439
15439
  signOut: () => changeSession(
15440
15440
  () => request("/v1/auth/logout", external_exports.object({ ok: external_exports.boolean() }), json3({}))
@@ -15445,7 +15445,7 @@ function createRelayClient(options) {
15445
15445
  observeProgress: (listener, options2) => {
15446
15446
  const observer = observeProgress(
15447
15447
  {
15448
- session: (signal2) => request("/v1/auth/session", sessionSchema.nullable(), { signal: signal2 }),
15448
+ session: (signal2) => request("/v1/auth/session", sessionSchema2.nullable(), { signal: signal2 }),
15449
15449
  progress: (session, signal2) => request("/v1/me/progress", memberProgressSchema, {
15450
15450
  signal: signal2,
15451
15451
  headers: { "X-Relay-Session": session }
@@ -15543,7 +15543,7 @@ function createRelayClient(options) {
15543
15543
  decide: (id2, input) => request(`/v1/attempts/${id2}/review`, attemptSchema, json3(input))
15544
15544
  };
15545
15545
  }
15546
- var interactionSchema, questTypeVersionSchema, collectionSlotSchema, questCollectionSchema, questInstanceSchema, questDraftChangesSchema, draftBase, questDraftSchema, questCatalogSchema, catalogDeploymentSchema, catalogDeploymentPreviewSchema, saveQuestDraftSchema, publishQuestDraftSchema, questViewSchema, collectionViewSchema, questExperienceSchema, questPublicationPreviewSchema, RelayError, common, claimInputSchema, draftSchema, recordSchema, QuestUpdatedError, authScopeSchema, principalSchema, sessionSchema, exchangeSchema, exchangeResultSchema, solanaAddressSchema, walletConnectionSchema, walletChallengeInputSchema, walletChallengeSchema, walletProofSchema, solanaBalanceSchema, collectionCodeSchema, collectionPassSchema, collectionResolveSchema, collectionBeginSchema, collectionPreviewSchema, projectDeploymentSchema, deploymentPreviewSchema, publishDeploymentSchema, deploymentResultSchema;
15546
+ var interactionSchema, questTypeVersionSchema, collectionSlotSchema, questCollectionSchema, questInstanceSchema, questDraftChangesSchema, draftBase, questDraftSchema, questCatalogSchema, catalogDeploymentSchema, catalogDeploymentPreviewSchema, saveQuestDraftSchema, publishQuestDraftSchema, questViewSchema, collectionViewSchema, questExperienceSchema, questPublicationPreviewSchema, RelayError, common, claimInputSchema, draftSchema, recordSchema, QuestUpdatedError, authScopeSchema, principalSchema, sessionSchema2, exchangeSchema, exchangeResultSchema, solanaAddressSchema, walletConnectionSchema, walletChallengeInputSchema, walletChallengeSchema, walletProofSchema, solanaBalanceSchema, collectionCodeSchema, collectionPassSchema, collectionResolveSchema, collectionBeginSchema, collectionPreviewSchema, projectDeploymentSchema, deploymentPreviewSchema, publishDeploymentSchema, deploymentResultSchema;
15547
15547
  var init_chunk_NC3UAP6T = __esm({
15548
15548
  "packages/sdk/dist/chunk-NC3UAP6T.js"() {
15549
15549
  "use strict";
@@ -15728,7 +15728,7 @@ var init_chunk_NC3UAP6T = __esm({
15728
15728
  external_exports.object({ kind: external_exports.literal("staff"), subject: identifier }).strict(),
15729
15729
  external_exports.object({ kind: external_exports.literal("operator"), subject: identifier }).strict()
15730
15730
  ]);
15731
- sessionSchema = authScopeSchema.extend({
15731
+ sessionSchema2 = authScopeSchema.extend({
15732
15732
  id: identifier,
15733
15733
  principal: principalSchema,
15734
15734
  expiresAt: external_exports.number()
@@ -15739,7 +15739,7 @@ var init_chunk_NC3UAP6T = __esm({
15739
15739
  }).strict();
15740
15740
  exchangeResultSchema = external_exports.object({
15741
15741
  token: external_exports.string(),
15742
- session: sessionSchema
15742
+ session: sessionSchema2
15743
15743
  });
15744
15744
  solanaAddressSchema = external_exports.string().min(32).max(44).regex(/^[1-9A-HJ-NP-Za-km-z]+$/);
15745
15745
  walletConnectionSchema = external_exports.object({
@@ -15891,7 +15891,7 @@ __export(dist_exports, {
15891
15891
  rewardKey: () => rewardKey,
15892
15892
  rewardSchema: () => rewardSchema,
15893
15893
  saveQuestDraftSchema: () => saveQuestDraftSchema,
15894
- sessionSchema: () => sessionSchema,
15894
+ sessionSchema: () => sessionSchema2,
15895
15895
  settingFieldSchema: () => settingFieldSchema,
15896
15896
  settings: () => settings,
15897
15897
  settingsFieldsSchema: () => settingsFieldsSchema,
@@ -15919,6 +15919,367 @@ var init_dist = __esm({
15919
15919
  }
15920
15920
  });
15921
15921
 
15922
+ // packages/sdk/dist/portal-proxy.js
15923
+ async function proxyParticipant(request, upstream) {
15924
+ const incoming = new URL(request.url);
15925
+ const path = incoming.pathname.slice("/relay".length);
15926
+ const allowed = incoming.pathname.startsWith("/relay/") && (/^\/v1\/auth\/(providers|connections|start|authorize|complete|session|logout|email\/start|email\/verify|callback\/(google|apple|x|discord))$/.test(
15927
+ path
15928
+ ) || /^\/v1\/me\/(progress|experience|attempts|evidence|pass|collection-pass|wallet(?:\/challenge|\/verify)?)$/.test(
15929
+ path
15930
+ ) || /^\/v1\/attempts\/[\w-]+(?:\/retry)?$/.test(path));
15931
+ if (!allowed || !["GET", "POST"].includes(request.method))
15932
+ return Response.json({ error: "Route not found" }, { status: 404 });
15933
+ const target = new URL(upstream.baseUrl);
15934
+ if (target.protocol !== "https:" || target.username || target.password || target.pathname !== "/" || target.search || target.hash)
15935
+ return Response.json(
15936
+ { error: "Participant API is not configured" },
15937
+ { status: 503 }
15938
+ );
15939
+ target.pathname = path;
15940
+ target.search = incoming.search;
15941
+ const headers = new Headers();
15942
+ for (const name of [
15943
+ "Cookie",
15944
+ "Origin",
15945
+ "Content-Type",
15946
+ "X-Relay-CSRF",
15947
+ "X-Relay-Session",
15948
+ "X-Relay-Upload-ID",
15949
+ "CF-Connecting-IP"
15950
+ ]) {
15951
+ const value = request.headers.get(name);
15952
+ if (value) headers.set(name, value);
15953
+ }
15954
+ const response = await upstream.fetch(target, {
15955
+ method: request.method,
15956
+ headers,
15957
+ body: request.method === "GET" ? void 0 : await request.arrayBuffer(),
15958
+ redirect: "manual"
15959
+ });
15960
+ const resultHeaders = new Headers(response.headers);
15961
+ resultHeaders.set("Cache-Control", "no-store");
15962
+ return new Response(response.body, {
15963
+ status: response.status,
15964
+ headers: resultHeaders
15965
+ });
15966
+ }
15967
+
15968
+ // apps/api/src/hosted-participant.ts
15969
+ init_zod();
15970
+ function hostedParticipantEnv(env, scope, origin) {
15971
+ return {
15972
+ ...env,
15973
+ LOCAL_DEVELOPMENT: "false",
15974
+ TEST_HOOKS: "false",
15975
+ DEMO_AUTH: "false",
15976
+ API_TOKEN: void 0,
15977
+ AUTH_EMULATORS: void 0,
15978
+ AUTH_ORGANIZATION: scope.organization,
15979
+ AUTH_PROJECT: scope.project,
15980
+ AUTH_ENVIRONMENT: "test",
15981
+ AUTH_ORIGINS: origin
15982
+ };
15983
+ }
15984
+ async function registeredStagingScope(origin, env) {
15985
+ const url2 = new URL(origin);
15986
+ if (url2.protocol !== "https:" || url2.origin !== origin || !env.STAGING_DOMAIN)
15987
+ return null;
15988
+ const suffix = `.${env.STAGING_DOMAIN}`;
15989
+ if (!url2.hostname.endsWith(suffix)) return null;
15990
+ const name = url2.hostname.slice(0, -suffix.length);
15991
+ if (!/^stage-[a-f0-9]{32}$/.test(name)) return null;
15992
+ const raw = name.slice(6);
15993
+ const id2 = `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`;
15994
+ const build = await env.MANAGEMENT.getByName("management-v1").activeBuild(id2);
15995
+ return build?.runtime === "hosted-test" ? { organization: build.organization, project: build.project } : null;
15996
+ }
15997
+ async function callbackEnvironment(request, env) {
15998
+ const url2 = new URL(request.url);
15999
+ if (request.method !== "GET") return env;
16000
+ const code = url2.pathname === "/v1/auth/authorize" ? url2.searchParams.get("code") : /^\/v1\/auth\/callback\/[a-z]+$/.test(url2.pathname) ? url2.searchParams.get("state") : null;
16001
+ if (!code) return env;
16002
+ const raw = await env.AUTH.getByName("auth-v1").readFlow(code);
16003
+ if (!raw) return env;
16004
+ const flow = external_exports.object({ origin: external_exports.string().url() }).safeParse(JSON.parse(raw));
16005
+ if (!flow.success) return env;
16006
+ const scope = await registeredStagingScope(flow.data.origin, env) ?? await registeredPreviewScope(flow.data.origin, env);
16007
+ return scope ? hostedParticipantEnv(env, scope, flow.data.origin) : env;
16008
+ }
16009
+
16010
+ // apps/api/src/development-preview.ts
16011
+ async function registeredPreviewScope(origin, env) {
16012
+ const url2 = new URL(origin);
16013
+ if (url2.protocol !== "https:" || url2.origin !== origin || !env.PREVIEW_DOMAIN || !env.PREVIEW_SESSIONS)
16014
+ return null;
16015
+ const suffix = `.${env.PREVIEW_DOMAIN}`;
16016
+ if (!url2.hostname.endsWith(suffix)) return null;
16017
+ const name = url2.hostname.slice(0, -suffix.length);
16018
+ if (!/^[a-z0-9-]+-[a-f0-9]{32}$/.test(name)) return null;
16019
+ const scope = await env.PREVIEW_SESSIONS.getByName(name.slice(-32)).resolve();
16020
+ return scope?.hostname === url2.hostname ? scope : null;
16021
+ }
16022
+ async function developmentPreview(request, env, participant) {
16023
+ const url2 = new URL(request.url);
16024
+ const scope = await registeredPreviewScope(url2.origin, env);
16025
+ if (!scope)
16026
+ return new Response(
16027
+ '<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Preview offline</title><body style="font:18px system-ui;max-width:38rem;margin:15vh auto;padding:24px"><h1>This preview is offline</h1><p>Ask its creator to start development again, or share a staged preview that stays online.</p><button onclick="location.reload()">Try again</button></body></html>',
16028
+ {
16029
+ status: 503,
16030
+ headers: {
16031
+ "Content-Type": "text/html; charset=utf-8",
16032
+ "Cache-Control": "no-store",
16033
+ "X-Robots-Tag": "noindex, nofollow"
16034
+ }
16035
+ }
16036
+ );
16037
+ if (url2.pathname.startsWith("/relay/"))
16038
+ return proxyParticipant(request, {
16039
+ baseUrl: "https://participant.internal",
16040
+ fetch: (input, init) => participant(
16041
+ new Request(input, init),
16042
+ hostedParticipantEnv(env, scope, url2.origin)
16043
+ )
16044
+ });
16045
+ if (url2.pathname === "/relay" || /^\/(management|__domino)(\/|$)/.test(url2.pathname))
16046
+ return new Response("Not found", { status: 404 });
16047
+ return new Response(null, {
16048
+ status: 204,
16049
+ headers: { "X-Domino-Preview-Origin": "allowed" }
16050
+ });
16051
+ }
16052
+
16053
+ // apps/api/src/preview-session.ts
16054
+ init_zod();
16055
+ import { DurableObject } from "cloudflare:workers";
16056
+
16057
+ // apps/api/src/preview-tunnel.ts
16058
+ init_zod();
16059
+ var PreviewTunnel = class {
16060
+ constructor(env) {
16061
+ this.env = env;
16062
+ }
16063
+ async call(path, method = "GET", body) {
16064
+ const response = await fetch(
16065
+ `https://api.cloudflare.com/client/v4${path}`,
16066
+ {
16067
+ method,
16068
+ headers: {
16069
+ Authorization: `Bearer ${this.env.PREVIEW_CF_TOKEN}`,
16070
+ "Content-Type": "application/json"
16071
+ },
16072
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
16073
+ signal: AbortSignal.timeout(1e4)
16074
+ }
16075
+ );
16076
+ if (response.status === 404 && method === "DELETE") return null;
16077
+ if (!response.ok)
16078
+ throw new Error(
16079
+ `Preview infrastructure request failed (${response.status}). Contact a Domino operator.`
16080
+ );
16081
+ const result = external_exports.object({ success: external_exports.boolean(), result: external_exports.unknown() }).parse(await response.json());
16082
+ if (!result.success)
16083
+ throw new Error(
16084
+ "Preview infrastructure request failed. Contact a Domino operator."
16085
+ );
16086
+ return result.result;
16087
+ }
16088
+ get base() {
16089
+ return `/accounts/${this.env.PREVIEW_CF_ACCOUNT}/cfd_tunnel`;
16090
+ }
16091
+ async lookup(name) {
16092
+ const tunnels = external_exports.array(external_exports.object({ id: external_exports.string().uuid() })).parse(
16093
+ await this.call(
16094
+ `${this.base}?name=${encodeURIComponent(name)}&is_deleted=false`
16095
+ )
16096
+ );
16097
+ return tunnels[0]?.id ?? null;
16098
+ }
16099
+ async ensure(name, hostname3, port) {
16100
+ const existing = external_exports.array(external_exports.object({ id: external_exports.string().uuid() })).parse(
16101
+ await this.call(
16102
+ `${this.base}?name=${encodeURIComponent(name)}&is_deleted=false`
16103
+ )
16104
+ );
16105
+ const tunnel = existing[0] ?? external_exports.object({ id: external_exports.string().uuid() }).parse(
16106
+ await this.call(this.base, "POST", {
16107
+ name,
16108
+ config_src: "cloudflare"
16109
+ })
16110
+ );
16111
+ await this.call(`${this.base}/${tunnel.id}/configurations`, "PUT", {
16112
+ config: {
16113
+ ingress: [
16114
+ {
16115
+ hostname: hostname3,
16116
+ service: `http://127.0.0.1:${port}`,
16117
+ originRequest: { httpHostHeader: hostname3 }
16118
+ },
16119
+ { service: "http_status:404" }
16120
+ ]
16121
+ }
16122
+ });
16123
+ const dnsBase = `/zones/${this.env.PREVIEW_CF_ZONE}/dns_records`;
16124
+ const records = external_exports.array(
16125
+ external_exports.object({ id: external_exports.string(), type: external_exports.string(), content: external_exports.string() })
16126
+ ).parse(
16127
+ await this.call(`${dnsBase}?name=${encodeURIComponent(hostname3)}`)
16128
+ );
16129
+ const content = `${tunnel.id}.cfargotunnel.com`;
16130
+ if (records.some(
16131
+ (record2) => record2.type !== "CNAME" || record2.content !== content
16132
+ ))
16133
+ throw new Error(
16134
+ "Preview hostname conflicts with an existing DNS record."
16135
+ );
16136
+ if (!records.length)
16137
+ await this.call(dnsBase, "POST", {
16138
+ type: "CNAME",
16139
+ name: hostname3,
16140
+ content,
16141
+ proxied: true,
16142
+ ttl: 1
16143
+ });
16144
+ return tunnel.id;
16145
+ }
16146
+ async token(id2) {
16147
+ return external_exports.string().min(1).parse(await this.call(`${this.base}/${id2}/token`));
16148
+ }
16149
+ async remove(id2, hostname3) {
16150
+ const base3 = `/zones/${this.env.PREVIEW_CF_ZONE}/dns_records`;
16151
+ const records = external_exports.array(
16152
+ external_exports.object({ id: external_exports.string(), type: external_exports.string(), content: external_exports.string() })
16153
+ ).parse(await this.call(`${base3}?name=${encodeURIComponent(hostname3)}`));
16154
+ for (const record2 of records) {
16155
+ if (record2.type !== "CNAME" || record2.content !== `${id2}.cfargotunnel.com`)
16156
+ throw new Error("Preview DNS ownership changed; cleanup stopped.");
16157
+ await this.call(`${base3}/${record2.id}`, "DELETE");
16158
+ }
16159
+ await this.call(`${this.base}/${id2}`, "DELETE");
16160
+ }
16161
+ async revoke(id2) {
16162
+ const secret = btoa(
16163
+ String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))
16164
+ );
16165
+ await this.call(`${this.base}/${id2}`, "PATCH", { tunnel_secret: secret });
16166
+ await this.call(`${this.base}/${id2}/connections`, "DELETE");
16167
+ }
16168
+ };
16169
+ function previewConfigurationIssues(env) {
16170
+ return Object.entries({
16171
+ PREVIEW_SESSIONS: env.PREVIEW_SESSIONS,
16172
+ PREVIEW_DOMAIN: env.PREVIEW_DOMAIN,
16173
+ PREVIEW_CF_TOKEN: env.PREVIEW_CF_TOKEN,
16174
+ PREVIEW_CF_ACCOUNT: env.PREVIEW_CF_ACCOUNT,
16175
+ PREVIEW_CF_ZONE: env.PREVIEW_CF_ZONE,
16176
+ PREVIEW_TLS_READY: env.PREVIEW_TLS_READY === "true"
16177
+ }).filter(([, value]) => !value).map(([key]) => key);
16178
+ }
16179
+
16180
+ // apps/api/src/preview-session.ts
16181
+ var sessionSchema = external_exports.object({
16182
+ organization: external_exports.string(),
16183
+ project: external_exports.string(),
16184
+ actor: external_exports.string(),
16185
+ hostname: external_exports.string(),
16186
+ generation: external_exports.string().uuid(),
16187
+ port: external_exports.number().int().min(1).max(65535),
16188
+ expiresAt: external_exports.number(),
16189
+ tunnel: external_exports.string().uuid().nullable()
16190
+ });
16191
+ var leaseMs = 9e4;
16192
+ var PreviewSession = class extends DurableObject {
16193
+ async read() {
16194
+ const value = await this.ctx.storage.get("session");
16195
+ return value ? sessionSchema.parse(value) : null;
16196
+ }
16197
+ async resolve() {
16198
+ const session = await this.read();
16199
+ return session && session.expiresAt > Date.now() && session.tunnel ? {
16200
+ organization: session.organization,
16201
+ project: session.project,
16202
+ hostname: session.hostname
16203
+ } : null;
16204
+ }
16205
+ async start(input) {
16206
+ return this.ctx.blockConcurrencyWhile(async () => {
16207
+ const old = await this.read();
16208
+ if (old && old.expiresAt > Date.now() && old.generation !== input.generation)
16209
+ return {
16210
+ error: "This worktree already has a running preview. Stop it or wait 90 seconds for its lease to expire."
16211
+ };
16212
+ const provider = new PreviewTunnel(this.env);
16213
+ const next = {
16214
+ ...input,
16215
+ expiresAt: 0,
16216
+ tunnel: old?.tunnel ?? null
16217
+ };
16218
+ await this.ctx.storage.put("session", next);
16219
+ await this.ctx.storage.setAlarm(Date.now() + leaseMs);
16220
+ const tunnel = await provider.ensure(
16221
+ `domino-${input.hostname.split(".")[0]}`,
16222
+ input.hostname,
16223
+ input.port
16224
+ );
16225
+ if (!old || old.generation !== input.generation || old.expiresAt <= Date.now())
16226
+ await provider.revoke(tunnel);
16227
+ const token = await provider.token(tunnel);
16228
+ await this.ctx.storage.put("session", {
16229
+ ...next,
16230
+ tunnel,
16231
+ expiresAt: Date.now() + leaseMs
16232
+ });
16233
+ await this.ctx.storage.delete("retireAt");
16234
+ await this.ctx.storage.setAlarm(Date.now() + leaseMs);
16235
+ return { url: `https://${input.hostname}`, token, leaseMs };
16236
+ });
16237
+ }
16238
+ async heartbeat(generation) {
16239
+ return this.ctx.blockConcurrencyWhile(async () => {
16240
+ const session = await this.read();
16241
+ if (!session || session.generation !== generation || session.expiresAt <= Date.now())
16242
+ return false;
16243
+ session.expiresAt = Date.now() + leaseMs;
16244
+ await this.ctx.storage.put("session", session);
16245
+ await this.ctx.storage.setAlarm(session.expiresAt);
16246
+ return true;
16247
+ });
16248
+ }
16249
+ async stop(generation) {
16250
+ return this.ctx.blockConcurrencyWhile(async () => {
16251
+ const session = await this.read();
16252
+ if (!session || session.generation !== generation) return;
16253
+ await this.ctx.storage.put("session", { ...session, expiresAt: 0 });
16254
+ await this.ctx.storage.setAlarm(Date.now() + 1e3);
16255
+ });
16256
+ }
16257
+ async alarm() {
16258
+ await this.ctx.blockConcurrencyWhile(async () => {
16259
+ const session = await this.read();
16260
+ if (!session || session.expiresAt > Date.now()) return;
16261
+ const provider = new PreviewTunnel(this.env);
16262
+ const tunnel = session.tunnel ?? await provider.lookup(`domino-${session.hostname.split(".")[0]}`);
16263
+ if (!tunnel) return;
16264
+ await this.ctx.storage.put("session", { ...session, tunnel });
16265
+ await provider.revoke(tunnel);
16266
+ const retireAt = await this.ctx.storage.get("retireAt") ?? Date.now() + 7 * 864e5;
16267
+ if (retireAt <= Date.now()) {
16268
+ await provider.remove(tunnel, session.hostname);
16269
+ await this.ctx.storage.put("session", {
16270
+ ...session,
16271
+ tunnel: null,
16272
+ expiresAt: 0
16273
+ });
16274
+ await this.ctx.storage.delete("retireAt");
16275
+ } else {
16276
+ await this.ctx.storage.put("retireAt", retireAt);
16277
+ await this.ctx.storage.setAlarm(retireAt);
16278
+ }
16279
+ });
16280
+ }
16281
+ };
16282
+
15922
16283
  // apps/api/src/index.ts
15923
16284
  import { WorkerEntrypoint as WorkerEntrypoint3 } from "cloudflare:workers";
15924
16285
 
@@ -15932,6 +16293,7 @@ init_zod();
15932
16293
  init_zod();
15933
16294
  init_dist();
15934
16295
  init_dist();
16296
+ init_zod();
15935
16297
  var repositorySchema = external_exports.object({
15936
16298
  id: external_exports.string().uuid(),
15937
16299
  organization: identifier,
@@ -15960,6 +16322,7 @@ var stageRequestSchema = external_exports.object({
15960
16322
  requestId: external_exports.string().uuid()
15961
16323
  }).strict();
15962
16324
  var buildIdentitySchema = external_exports.object({
16325
+ runtime: external_exports.enum(["simulation", "hosted-test"]).default("simulation"),
15963
16326
  id: external_exports.string().uuid(),
15964
16327
  repository: external_exports.string().uuid(),
15965
16328
  organization: identifier,
@@ -16011,6 +16374,26 @@ var buildArtifactsSchema = external_exports.object({
16011
16374
  if (value.assets.reduce((sum, a) => sum + a.size, 0) > 1e7)
16012
16375
  ctx.addIssue({ code: "custom", message: "Staging assets exceed 10 MB" });
16013
16376
  });
16377
+ var developmentPreflightSchema = external_exports.object({
16378
+ organization: external_exports.string(),
16379
+ project: external_exports.string(),
16380
+ environment: external_exports.enum(["test", "live"]),
16381
+ staging: external_exports.enum(["configured", "blocked"]),
16382
+ development: external_exports.object({
16383
+ status: external_exports.enum(["configured", "blocked"]),
16384
+ missing: external_exports.array(external_exports.string()),
16385
+ authentication: external_exports.array(external_exports.string())
16386
+ }),
16387
+ checks: external_exports.array(
16388
+ external_exports.object({
16389
+ id: external_exports.string(),
16390
+ status: external_exports.enum(["ok", "error"]),
16391
+ message: external_exports.string(),
16392
+ missing: external_exports.array(external_exports.string()).optional(),
16393
+ issues: external_exports.array(external_exports.string()).optional()
16394
+ })
16395
+ )
16396
+ });
16014
16397
  var environmentSchema = external_exports.enum(["test", "live"]);
16015
16398
  var roleSchema = external_exports.enum(["editor", "reviewer", "staff"]);
16016
16399
  var permissionSchema = external_exports.enum([
@@ -16027,7 +16410,12 @@ var rolePermissions = {
16027
16410
  reviewer: ["read", "review"],
16028
16411
  staff: ["read", "confirm", "handover"]
16029
16412
  };
16413
+ var projectSetupSchema = external_exports.object({
16414
+ mode: external_exports.enum(["new", "existing"]),
16415
+ brief: external_exports.string().trim().min(1).max(4e3)
16416
+ }).strict();
16030
16417
  var projectSchema = external_exports.object({
16418
+ setup: projectSetupSchema.optional(),
16031
16419
  id: external_exports.string().min(1),
16032
16420
  organization: identifier,
16033
16421
  project: identifier,
@@ -16038,7 +16426,11 @@ var projectSchema = external_exports.object({
16038
16426
  permissions: external_exports.array(permissionSchema),
16039
16427
  environments: external_exports.array(environmentSchema)
16040
16428
  });
16041
- var projectInputSchema = external_exports.object({ project: identifier, name: external_exports.string().trim().min(1).max(80) }).strict();
16429
+ var projectInputSchema = external_exports.object({
16430
+ project: identifier,
16431
+ name: external_exports.string().trim().min(1).max(80),
16432
+ setup: projectSetupSchema.optional()
16433
+ }).strict();
16042
16434
  var grantSchema = external_exports.object({
16043
16435
  subject: external_exports.string().min(1).max(200),
16044
16436
  roles: external_exports.array(roleSchema),
@@ -16181,52 +16573,6 @@ var tokenSchema = external_exports.object({
16181
16573
  // apps/api/src/staging.ts
16182
16574
  init_dist();
16183
16575
 
16184
- // packages/sdk/dist/portal-proxy.js
16185
- async function proxyParticipant(request, upstream) {
16186
- const incoming = new URL(request.url);
16187
- const path = incoming.pathname.slice("/relay".length);
16188
- const allowed = incoming.pathname.startsWith("/relay/") && (/^\/v1\/auth\/(providers|connections|start|authorize|complete|session|logout|email\/start|email\/verify|callback\/(google|apple|x|discord))$/.test(
16189
- path
16190
- ) || /^\/v1\/me\/(progress|experience|attempts|evidence|pass|collection-pass|wallet(?:\/challenge|\/verify)?)$/.test(
16191
- path
16192
- ) || /^\/v1\/attempts\/[\w-]+(?:\/retry)?$/.test(path));
16193
- if (!allowed || !["GET", "POST"].includes(request.method))
16194
- return Response.json({ error: "Route not found" }, { status: 404 });
16195
- const target = new URL(upstream.baseUrl);
16196
- if (target.protocol !== "https:" || target.username || target.password || target.pathname !== "/" || target.search || target.hash)
16197
- return Response.json(
16198
- { error: "Participant API is not configured" },
16199
- { status: 503 }
16200
- );
16201
- target.pathname = path;
16202
- target.search = incoming.search;
16203
- const headers = new Headers();
16204
- for (const name of [
16205
- "Cookie",
16206
- "Origin",
16207
- "Content-Type",
16208
- "X-Relay-CSRF",
16209
- "X-Relay-Session",
16210
- "X-Relay-Upload-ID",
16211
- "CF-Connecting-IP"
16212
- ]) {
16213
- const value = request.headers.get(name);
16214
- if (value) headers.set(name, value);
16215
- }
16216
- const response = await upstream.fetch(target, {
16217
- method: request.method,
16218
- headers,
16219
- body: request.method === "GET" ? void 0 : await request.arrayBuffer(),
16220
- redirect: "manual"
16221
- });
16222
- const resultHeaders = new Headers(response.headers);
16223
- resultHeaders.set("Cache-Control", "no-store");
16224
- return new Response(response.body, {
16225
- status: response.status,
16226
- headers: resultHeaders
16227
- });
16228
- }
16229
-
16230
16576
  // apps/api/src/quest-catalog.ts
16231
16577
  init_dist();
16232
16578
 
@@ -17048,7 +17394,7 @@ var ManagementReads = class {
17048
17394
  // apps/api/src/ledger.ts
17049
17395
  init_zod();
17050
17396
  init_dist();
17051
- import { DurableObject } from "cloudflare:workers";
17397
+ import { DurableObject as DurableObject2 } from "cloudflare:workers";
17052
17398
 
17053
17399
  // apps/api/src/quest-graph.ts
17054
17400
  init_dist();
@@ -17391,7 +17737,7 @@ var esm_default2 = esm_default(ALPHABET);
17391
17737
  init_zod();
17392
17738
  init_dist();
17393
17739
  var storedWalletChallenge = walletChallengeSchema.extend({
17394
- session: sessionSchema,
17740
+ session: sessionSchema2,
17395
17741
  origin: external_exports.string().url()
17396
17742
  });
17397
17743
  function solanaPublicKey(address) {
@@ -17507,7 +17853,7 @@ function json2(value) {
17507
17853
  function parse3(schema, data) {
17508
17854
  return schema.parse(JSON.parse(data));
17509
17855
  }
17510
- var OrganizationLedger = class extends DurableObject {
17856
+ var OrganizationLedger = class extends DurableObject2 {
17511
17857
  sql;
17512
17858
  management;
17513
17859
  catalog;
@@ -20541,8 +20887,8 @@ async function profileFromStoredGrant(env, sealed, subject) {
20541
20887
 
20542
20888
  // apps/api/src/auth.ts
20543
20889
  init_dist();
20544
- import { DurableObject as DurableObject2 } from "cloudflare:workers";
20545
- var AuthStore = class extends DurableObject2 {
20890
+ import { DurableObject as DurableObject3 } from "cloudflare:workers";
20891
+ var AuthStore = class extends DurableObject3 {
20546
20892
  constructor(ctx, env) {
20547
20893
  super(ctx, env);
20548
20894
  ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS identities (scope TEXT NOT NULL, issuer TEXT NOT NULL, subject TEXT NOT NULL, member TEXT NOT NULL, PRIMARY KEY(scope,issuer,subject));
@@ -20852,7 +21198,7 @@ var AuthStore = class extends DurableObject2 {
20852
21198
  id2,
20853
21199
  Date.now()
20854
21200
  ).toArray()[0];
20855
- return row ? sessionSchema.parse(JSON.parse(row.data)) : null;
21201
+ return row ? sessionSchema2.parse(JSON.parse(row.data)) : null;
20856
21202
  }
20857
21203
  linkAccount(session, issuer, subject, provider, sealed, login, profile) {
20858
21204
  if (session.principal.kind !== "member")
@@ -20923,7 +21269,7 @@ var AuthStore = class extends DurableObject2 {
20923
21269
  await hash2(token),
20924
21270
  Date.now()
20925
21271
  ).toArray()[0];
20926
- return row ? sessionSchema.parse(JSON.parse(row.data)) : null;
21272
+ return row ? sessionSchema2.parse(JSON.parse(row.data)) : null;
20927
21273
  }
20928
21274
  async revoke(token) {
20929
21275
  this.ctx.storage.sql.exec(
@@ -21195,33 +21541,32 @@ var HostingControl = class extends WorkerEntrypoint2 {
21195
21541
  const artifacts = buildArtifactsSchema.parse(await object2.json());
21196
21542
  const releases = batchReleaseSchema.parse(artifacts.quests);
21197
21543
  const catalog = catalogDeploymentSchema.omit({ expectedRevision: true }).parse(artifacts.catalog);
21198
- const ledger = this.env.STAGING_LEDGER.getByName(
21199
- JSON.stringify([build.organization, "test", previewProject(build.id)])
21544
+ const ledger = (build.runtime === "hosted-test" ? this.env.LEDGER : this.env.STAGING_LEDGER).getByName(
21545
+ JSON.stringify([
21546
+ build.organization,
21547
+ "test",
21548
+ build.runtime === "hosted-test" ? build.project : previewProject(build.id)
21549
+ ])
21200
21550
  );
21201
21551
  const headers = {
21202
21552
  "Content-Type": "application/json",
21203
21553
  "X-Relay-Actor": "management",
21204
21554
  "X-Relay-Actor-ID": build.actor
21205
21555
  };
21206
- if (releases.releases.length) {
21207
- const published = await ledger.fetch(
21208
- new Request("https://ledger/v1/releases/batch", {
21209
- method: "POST",
21210
- headers,
21211
- body: JSON.stringify(releases)
21212
- })
21213
- );
21214
- if (!published.ok) throw new Error(await published.text());
21215
- }
21216
- const current = await ledger.fetch(
21217
- new Request("https://ledger/management/v1/catalog", { headers })
21556
+ const preview = await ledger.fetch(
21557
+ new Request("https://ledger/management/v1/deployments/preview", {
21558
+ method: "POST",
21559
+ headers,
21560
+ body: JSON.stringify({ ...releases, ...catalog })
21561
+ })
21218
21562
  );
21219
- const { revision } = external_exports.object({ revision: external_exports.number() }).parse(await current.json());
21563
+ if (!preview.ok) throw new Error(await preview.text());
21564
+ const { id: id2 } = external_exports.object({ id: external_exports.string() }).parse(await preview.json());
21220
21565
  const deployed = await ledger.fetch(
21221
- new Request("https://ledger/management/v1/catalog/deploy", {
21566
+ new Request("https://ledger/management/v1/deployments/publish", {
21222
21567
  method: "POST",
21223
21568
  headers,
21224
- body: JSON.stringify({ ...catalog, expectedRevision: revision })
21569
+ body: JSON.stringify({ id: id2 })
21225
21570
  })
21226
21571
  );
21227
21572
  if (!deployed.ok) throw new Error(await deployed.text());
@@ -21250,6 +21595,15 @@ async function stagingRequest(request, env, participant) {
21250
21595
  if (!build || !env.STAGING_APPS || !env.STAGING_AUTH || !env.STAGING_LEDGER)
21251
21596
  return new Response("No staging build is ready yet", { status: 404 });
21252
21597
  if (url2.pathname.startsWith("/relay/")) {
21598
+ if (build.runtime === "hosted-test") {
21599
+ return proxyParticipant(request, {
21600
+ baseUrl: "https://participant.internal",
21601
+ fetch: (input, init) => participant(
21602
+ new Request(input, init),
21603
+ hostedParticipantEnv(env, build, url2.origin)
21604
+ )
21605
+ });
21606
+ }
21253
21607
  if (url2.pathname.startsWith("/relay/v1/auth/") && !["/relay/v1/auth/session", "/relay/v1/auth/logout"].includes(
21254
21608
  url2.pathname
21255
21609
  ))
@@ -21332,7 +21686,7 @@ async function stagingRequest(request, env, participant) {
21332
21686
  return response.headers.get("Content-Type")?.includes("text/html") ? new HTMLRewriter().on("body", {
21333
21687
  element(element) {
21334
21688
  element.prepend(
21335
- '<aside style="position:sticky;top:0;z-index:99999;padding:10px;background:#fff3cd;color:#332701;text-align:center;font:14px system-ui">Staging preview \xB7 simulated participants \xB7 no live data</aside>',
21689
+ '<aside style="position:sticky;top:0;z-index:99999;padding:10px;background:#fff3cd;color:#332701;text-align:center;font:14px system-ui">Staging preview \xB7 ' + (build.runtime === "hosted-test" ? "test data \xB7 real sign-in" : "simulated participants \xB7 no live data") + "</aside>",
21336
21690
  { html: true }
21337
21691
  );
21338
21692
  }
@@ -22594,6 +22948,24 @@ function localParticipantRequest(request, env) {
22594
22948
  return env.LOCAL_DEVELOPMENT === "true" && ["localhost", "127.0.0.1", "[::1]"].includes(new URL(request.url).hostname);
22595
22949
  }
22596
22950
 
22951
+ // apps/api/src/development-preflight.ts
22952
+ function stagingConfigurationIssues(env) {
22953
+ const required2 = {
22954
+ BUILDER: env.BUILDER,
22955
+ HOSTING_ASSETS: env.HOSTING_ASSETS,
22956
+ STAGING_DOMAIN: env.STAGING_DOMAIN,
22957
+ STAGING_APPS: env.STAGING_APPS,
22958
+ STAGING_AUTH: env.STAGING_AUTH,
22959
+ STAGING_LEDGER: env.STAGING_LEDGER,
22960
+ GITHUB_APP_ID: env.GITHUB_APP_ID,
22961
+ GITHUB_APP_PRIVATE_KEY: env.GITHUB_APP_PRIVATE_KEY,
22962
+ GITHUB_INSTALLATION_ID: env.GITHUB_INSTALLATION_ID,
22963
+ GITHUB_REPOSITORY_OWNER: env.GITHUB_REPOSITORY_OWNER,
22964
+ GIT_PUBLIC_URL: env.GIT_PUBLIC_URL
22965
+ };
22966
+ return Object.entries(required2).filter(([, value]) => !value).map(([key]) => key);
22967
+ }
22968
+
22597
22969
  // apps/api/src/readiness.ts
22598
22970
  function launchReadiness(env, releases, scope) {
22599
22971
  const issues = productionParticipantIssues(env);
@@ -32352,6 +32724,128 @@ async function managementRoutes(request, env) {
32352
32724
  }
32353
32725
  if (!organization || !(await store.projects(organization)).some((p) => p.project === project))
32354
32726
  throw new AccessError("Project not found", 404);
32727
+ if (path === "/development/session" && ["POST", "PUT", "DELETE"].includes(request.method)) {
32728
+ requirePermission("publish");
32729
+ if (environment !== "test")
32730
+ throw new AccessError("Development requires the test environment", 422);
32731
+ if (previewConfigurationIssues(env).length || !env.PREVIEW_SESSIONS || !env.PREVIEW_DOMAIN)
32732
+ throw new AccessError(
32733
+ "Shared development previews are not configured. A Domino operator must enable preview hosting. Use --offline only for isolated platform development.",
32734
+ 503
32735
+ );
32736
+ const input = external_exports.object({
32737
+ workspace: external_exports.string().uuid(),
32738
+ generation: external_exports.string().uuid(),
32739
+ port: external_exports.number().int().min(1).max(65535).optional()
32740
+ }).strict().parse(await request.json());
32741
+ const id2 = (await hash2(
32742
+ JSON.stringify([organization, project, actor, input.workspace])
32743
+ )).slice(0, 32);
32744
+ const session = env.PREVIEW_SESSIONS.getByName(id2);
32745
+ if (request.method === "DELETE") {
32746
+ await session.stop(input.generation);
32747
+ return respond({ stopped: true });
32748
+ }
32749
+ if (request.method === "PUT") {
32750
+ if (!await session.heartbeat(input.generation))
32751
+ throw new AccessError(
32752
+ "Preview lease expired. Restart domino dev.",
32753
+ 409
32754
+ );
32755
+ return respond({ renewed: true });
32756
+ }
32757
+ if (!input.port) throw new AccessError("An app port is required", 400);
32758
+ if (!await env.AUTH.getByName("auth-v1").rateLimit(
32759
+ `preview-start:${organization}:${actor}`,
32760
+ 10,
32761
+ 6e4
32762
+ ))
32763
+ throw new AccessError(
32764
+ "Too many preview starts. Try again shortly.",
32765
+ 429
32766
+ );
32767
+ const hostname3 = `${project.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 24)}-${id2}.${env.PREVIEW_DOMAIN}`;
32768
+ const issues = productionParticipantIssues(
32769
+ hostedParticipantEnv(
32770
+ env,
32771
+ { organization, project },
32772
+ `https://${hostname3}`
32773
+ )
32774
+ );
32775
+ if (issues.length)
32776
+ throw new AccessError(
32777
+ "Hosted test sign-in is not configured. A Domino operator must enable participant authentication.",
32778
+ 503
32779
+ );
32780
+ const result = await session.start({
32781
+ organization,
32782
+ project,
32783
+ actor,
32784
+ hostname: hostname3,
32785
+ generation: input.generation,
32786
+ port: input.port
32787
+ });
32788
+ if ("error" in result)
32789
+ throw new AccessError(
32790
+ result.error ?? "Preview is already running",
32791
+ 409
32792
+ );
32793
+ return respond(result);
32794
+ }
32795
+ if (path === "/preflight" && request.method === "GET") {
32796
+ requirePermission("read");
32797
+ const missing = stagingConfigurationIssues(env);
32798
+ const repository = await store.repository(organization, project);
32799
+ const authIssues = productionParticipantIssues(
32800
+ hostedParticipantEnv(
32801
+ env,
32802
+ { organization, project },
32803
+ `https://preflight.${env.PREVIEW_DOMAIN ?? "preview.domino.run"}`
32804
+ )
32805
+ );
32806
+ const checks = [
32807
+ {
32808
+ id: "hosted-test-auth",
32809
+ status: authIssues.length ? "error" : "ok",
32810
+ message: authIssues.length ? "A Domino operator must configure real participant sign-in." : "Real sign-in is configured; complete a sign-in to verify provider availability.",
32811
+ issues: authIssues
32812
+ },
32813
+ {
32814
+ id: "project-access",
32815
+ status: "ok",
32816
+ message: "Project access verified."
32817
+ },
32818
+ {
32819
+ id: "publication",
32820
+ status: permissions.includes("publish") ? "ok" : "error",
32821
+ message: permissions.includes("publish") ? "Publication permission verified." : "Ask a project administrator for publication permission."
32822
+ },
32823
+ {
32824
+ id: "cloud-builds",
32825
+ status: missing.length ? "error" : "ok",
32826
+ message: missing.length ? "A Domino operator must configure cloud hosting before this project can stage." : "Cloud hosting configuration is present. A successful build has not been verified.",
32827
+ missing
32828
+ },
32829
+ {
32830
+ id: "repository",
32831
+ status: repository?.status === "ready" ? "ok" : "error",
32832
+ message: repository?.status === "ready" ? "Hosted repository is ready." : "Create a hosted repository before staging."
32833
+ }
32834
+ ];
32835
+ const developmentMissing = previewConfigurationIssues(env);
32836
+ return respond({
32837
+ organization,
32838
+ project,
32839
+ environment,
32840
+ development: {
32841
+ status: developmentMissing.length || authIssues.length ? "blocked" : "configured",
32842
+ missing: developmentMissing,
32843
+ authentication: authIssues
32844
+ },
32845
+ staging: checks.every((check2) => check2.status === "ok") ? "configured" : "blocked",
32846
+ checks
32847
+ });
32848
+ }
32355
32849
  if (path === "/repository" && ["GET", "POST"].includes(request.method)) {
32356
32850
  requirePermission(request.method === "POST" ? "publish" : "read");
32357
32851
  const publicBase = env.GIT_PUBLIC_URL;
@@ -32367,15 +32861,45 @@ async function managementRoutes(request, env) {
32367
32861
  if (repo.status !== "ready") {
32368
32862
  if (!env.HOSTING_ASSETS)
32369
32863
  throw new AccessError("Starter storage is not configured", 503);
32864
+ const active = await env.HOSTING_ASSETS.get(
32865
+ "starters/campaign-current.json"
32866
+ );
32867
+ const pointer = active ? external_exports.object({
32868
+ format: external_exports.literal(1),
32869
+ key: external_exports.string().regex(/^starters\/campaign\/[a-f0-9]{64}\.json$/),
32870
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/)
32871
+ }).parse(await active.json()) : null;
32370
32872
  const starter = await env.HOSTING_ASSETS.get(
32371
- "starters/campaign.json"
32873
+ pointer?.key ?? "starters/campaign.json"
32372
32874
  );
32373
32875
  if (!starter)
32374
32876
  throw new AccessError(
32375
32877
  "Campaign starter has not been uploaded",
32376
32878
  503
32377
32879
  );
32378
- const files = starterFilesSchema.parse(await starter.json());
32880
+ const contents = await starter.text();
32881
+ if (pointer && await hash2(contents) !== pointer.sha256)
32882
+ throw new AccessError(
32883
+ "Starter integrity check failed. Contact a Domino operator.",
32884
+ 503
32885
+ );
32886
+ const files = starterFilesSchema.parse(JSON.parse(contents));
32887
+ const setup = (await store.projects(organization)).find(
32888
+ (item) => item.project === project
32889
+ )?.setup;
32890
+ if (setup) {
32891
+ const brief = {
32892
+ path: "CAMPAIGN.md",
32893
+ encoding: "utf-8",
32894
+ content: `# Project brief
32895
+
32896
+ ${setup.brief}
32897
+ `
32898
+ };
32899
+ const index = files.findIndex((file2) => file2.path === brief.path);
32900
+ if (index < 0) files.push(brief);
32901
+ else files[index] = brief;
32902
+ }
32379
32903
  const github = new GitHubRepositories(env);
32380
32904
  const provider = await github.provision(repo.id);
32381
32905
  await github.seed(provider, files);
@@ -32403,12 +32927,26 @@ async function managementRoutes(request, env) {
32403
32927
  "Staging builds require the test environment",
32404
32928
  422
32405
32929
  );
32406
- if (!env.BUILDER || !env.HOSTING_ASSETS || !env.STAGING_DOMAIN)
32407
- throw new AccessError("Cloud builds are not configured", 503);
32930
+ if (stagingConfigurationIssues(env).length || !env.BUILDER || !env.HOSTING_ASSETS)
32931
+ throw new AccessError(
32932
+ "Cloud builds are not configured. Run domino doctor --remote; a Domino operator must enable cloud hosting.",
32933
+ 503
32934
+ );
32408
32935
  const repository = await store.repository(organization, project);
32409
32936
  if (!repository || repository.status !== "ready")
32410
32937
  throw new AccessError("Create a hosted repository first", 409);
32411
32938
  if (path === "/builds" && request.method === "POST") {
32939
+ if (productionParticipantIssues(
32940
+ hostedParticipantEnv(
32941
+ env,
32942
+ { organization, project },
32943
+ `https://stage-${repository.id.replaceAll("-", "")}.${env.STAGING_DOMAIN}`
32944
+ )
32945
+ ).length)
32946
+ throw new AccessError(
32947
+ "Hosted test sign-in is not configured. Run domino doctor --remote before staging.",
32948
+ 503
32949
+ );
32412
32950
  const input = stageRequestSchema.parse(await request.json());
32413
32951
  await new GitHubRepositories(env).verifyCommit(
32414
32952
  repository.provider,
@@ -32416,6 +32954,7 @@ async function managementRoutes(request, env) {
32416
32954
  );
32417
32955
  const build2 = await store.reserveBuild({
32418
32956
  id: input.requestId,
32957
+ runtime: "hosted-test",
32419
32958
  repository: repository.id,
32420
32959
  organization,
32421
32960
  project,
@@ -32877,7 +33416,7 @@ async function deviceRoutes(request, env) {
32877
33416
 
32878
33417
  // apps/api/src/management-store.ts
32879
33418
  init_zod();
32880
- import { DurableObject as DurableObject3 } from "cloudflare:workers";
33419
+ import { DurableObject as DurableObject4 } from "cloudflare:workers";
32881
33420
  var repositoryBase = external_exports.object({
32882
33421
  id: external_exports.string().uuid(),
32883
33422
  organization: external_exports.string(),
@@ -32893,11 +33432,12 @@ var repositoryRecord = external_exports.discriminatedUnion("status", [
32893
33432
  provider: githubRepositorySchema
32894
33433
  })
32895
33434
  ]);
32896
- var ManagementStore = class extends DurableObject3 {
33435
+ var ManagementStore = class extends DurableObject4 {
32897
33436
  constructor(ctx, env) {
32898
33437
  super(ctx, env);
32899
33438
  ctx.storage.sql.exec(`
32900
33439
  CREATE TABLE IF NOT EXISTS projects (organization TEXT, project TEXT, name TEXT NOT NULL, PRIMARY KEY(organization,project));
33440
+ CREATE TABLE IF NOT EXISTS project_setup (organization TEXT, project TEXT, data TEXT NOT NULL, PRIMARY KEY(organization,project));
32901
33441
  CREATE TABLE IF NOT EXISTS grants (organization TEXT, project TEXT, subject TEXT, data TEXT NOT NULL, PRIMARY KEY(organization,project,subject));
32902
33442
  DROP TABLE IF EXISTS tokens;
32903
33443
  CREATE TABLE IF NOT EXISTS audit (id INTEGER PRIMARY KEY AUTOINCREMENT, actor TEXT NOT NULL, organization TEXT NOT NULL, action TEXT NOT NULL, data TEXT NOT NULL, at INTEGER NOT NULL);
@@ -33160,12 +33700,14 @@ var ManagementStore = class extends DurableObject3 {
33160
33700
  return { total, audit };
33161
33701
  }
33162
33702
  projects(organization) {
33163
- return organization ? this.ctx.storage.sql.exec(
33164
- "SELECT * FROM projects WHERE organization=? ORDER BY name,project",
33165
- organization
33166
- ).toArray() : this.ctx.storage.sql.exec(
33167
- "SELECT * FROM projects ORDER BY organization,name,project"
33168
- ).toArray();
33703
+ const query = "SELECT p.*,s.data AS setup FROM projects p LEFT JOIN project_setup s ON p.organization=s.organization AND p.project=s.project";
33704
+ return this.ctx.storage.sql.exec(
33705
+ query + (organization ? " WHERE p.organization=?" : "") + " ORDER BY p.name,p.project",
33706
+ ...organization ? [organization] : []
33707
+ ).toArray().map(({ setup, ...project }) => ({
33708
+ ...project,
33709
+ ...setup ? { setup: projectSetupSchema.parse(JSON.parse(setup)) } : {}
33710
+ }));
33169
33711
  }
33170
33712
  create(organization, input, actor) {
33171
33713
  return this.ctx.storage.transactionSync(() => {
@@ -33173,7 +33715,7 @@ var ManagementStore = class extends DurableObject3 {
33173
33715
  (p) => p.project === input.project
33174
33716
  );
33175
33717
  if (existing) {
33176
- if (existing.name !== input.name)
33718
+ if (existing.name !== input.name || input.setup && JSON.stringify(existing.setup) !== JSON.stringify(input.setup))
33177
33719
  throw new Error("Project ID is already in use");
33178
33720
  return existing;
33179
33721
  }
@@ -33183,6 +33725,13 @@ var ManagementStore = class extends DurableObject3 {
33183
33725
  input.project,
33184
33726
  input.name
33185
33727
  );
33728
+ if (input.setup)
33729
+ this.ctx.storage.sql.exec(
33730
+ "INSERT INTO project_setup VALUES(?,?,?)",
33731
+ organization,
33732
+ input.project,
33733
+ JSON.stringify(input.setup)
33734
+ );
33186
33735
  this.audit(actor, organization, "project-created", input);
33187
33736
  return { organization, ...input };
33188
33737
  });
@@ -33263,6 +33812,7 @@ var photo_quest_default = { code: 'var Kd=Object.defineProperty;var ee=(e,r)=>{f
33263
33812
 
33264
33813
  // apps/api/src/auth-routes.ts
33265
33814
  init_zod();
33815
+ init_dist();
33266
33816
 
33267
33817
  // apps/api/src/oauth-routes.ts
33268
33818
  init_zod();
@@ -33281,7 +33831,7 @@ function authScope(env) {
33281
33831
  // apps/api/src/oauth-routes.ts
33282
33832
  var intentSchema = external_exports.discriminatedUnion("mode", [
33283
33833
  external_exports.object({ mode: external_exports.literal("sign-in"), scope: authScopeSchema }),
33284
- external_exports.object({ mode: external_exports.literal("connect"), session: sessionSchema })
33834
+ external_exports.object({ mode: external_exports.literal("connect"), session: sessionSchema2 })
33285
33835
  ]);
33286
33836
  var returnSchema = external_exports.object({
33287
33837
  origin: external_exports.string().url(),
@@ -33559,6 +34109,8 @@ async function oauthRoutes(request, env, session) {
33559
34109
 
33560
34110
  // apps/api/src/auth-routes.ts
33561
34111
  var emailFlowSchema = external_exports.object({
34112
+ scope: authScopeSchema,
34113
+ origin: external_exports.string(),
33562
34114
  email: external_exports.string().email(),
33563
34115
  digest: external_exports.string(),
33564
34116
  binding: external_exports.string()
@@ -33602,6 +34154,8 @@ async function authRoutes(request, env, session) {
33602
34154
  id2,
33603
34155
  {
33604
34156
  email: email3,
34157
+ scope: authScope(env),
34158
+ origin: request.headers.get("Origin") ?? "",
33605
34159
  digest: await hash2(id2 + ":" + code),
33606
34160
  binding: await hash2(binding)
33607
34161
  },
@@ -33614,6 +34168,8 @@ async function authRoutes(request, env, session) {
33614
34168
  id2,
33615
34169
  JSON.stringify({
33616
34170
  email: email3,
34171
+ scope: authScope(env),
34172
+ origin: request.headers.get("Origin") ?? "",
33617
34173
  digest: await hash2(id2 + ":" + code),
33618
34174
  binding: await hash2(binding)
33619
34175
  })
@@ -33638,7 +34194,12 @@ async function authRoutes(request, env, session) {
33638
34194
  const raw = await auth.readFlow(challenge);
33639
34195
  if (!raw || !await auth.rateLimit("verify:" + challenge, 5, 6e5))
33640
34196
  return fail("Code expired or too many attempts. Request another.");
33641
- const flow = emailFlowSchema.parse(JSON.parse(raw));
34197
+ const parsed = emailFlowSchema.safeParse(JSON.parse(raw));
34198
+ if (!parsed.success) return fail("Code expired. Request another.");
34199
+ const flow = parsed.data;
34200
+ const scope = authScope(env);
34201
+ if (flow.scope.organization !== scope.organization || flow.scope.project !== scope.project || flow.scope.environment !== scope.environment || flow.origin !== (request.headers.get("Origin") ?? ""))
34202
+ return fail("Sign-in scope changed. Request another code.");
33642
34203
  if (flow.binding !== await hash2(cookie(request, "relay_auth_flow")) || flow.digest !== await hash2(challenge + ":" + code))
33643
34204
  return fail("Invalid code");
33644
34205
  if (!await auth.consumeFlow(challenge, raw))
@@ -34067,6 +34628,11 @@ var gateway = {
34067
34628
  }
34068
34629
  }
34069
34630
  };
34631
+ var PreviewGateway = class extends WorkerEntrypoint3 {
34632
+ async fetch(request) {
34633
+ return developmentPreview(request, this.env, gateway.fetch);
34634
+ }
34635
+ };
34070
34636
  var StagingGateway = class extends WorkerEntrypoint3 {
34071
34637
  async fetch(request) {
34072
34638
  return await stagingRequest(request, this.env, gateway.fetch) ?? new Response("Staging not found", { status: 404 });
@@ -34074,7 +34640,10 @@ var StagingGateway = class extends WorkerEntrypoint3 {
34074
34640
  };
34075
34641
  var index_default = {
34076
34642
  async fetch(request, env) {
34077
- const response = await gateway.fetch(request, env);
34643
+ const response = await gateway.fetch(
34644
+ request,
34645
+ await callbackEnvironment(request, env)
34646
+ );
34078
34647
  const headers = new Headers(response.headers);
34079
34648
  headers.set("Cache-Control", "no-store");
34080
34649
  return new Response(response.body, { status: response.status, headers });
@@ -34086,6 +34655,8 @@ export {
34086
34655
  HostingControl,
34087
34656
  ManagementStore,
34088
34657
  OrganizationLedger,
34658
+ PreviewGateway,
34659
+ PreviewSession,
34089
34660
  StagingAuth,
34090
34661
  StagingGateway,
34091
34662
  StagingLedger,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domino-sdk/relay-runtime",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "Local development runtime for Domino Relay",
package/wrangler.jsonc CHANGED
@@ -28,6 +28,10 @@
28
28
  {
29
29
  "name": "STAGING_AUTH",
30
30
  "class_name": "StagingAuth"
31
+ },
32
+ {
33
+ "name": "PREVIEW_SESSIONS",
34
+ "class_name": "PreviewSession"
31
35
  }
32
36
  ]
33
37
  },
@@ -56,12 +60,22 @@
56
60
  "StagingLedger",
57
61
  "StagingAuth"
58
62
  ]
63
+ },
64
+ {
65
+ "tag": "v5-preview-sessions",
66
+ "new_sqlite_classes": [
67
+ "PreviewSession"
68
+ ]
59
69
  }
60
70
  ],
61
71
  "r2_buckets": [
62
72
  {
63
73
  "binding": "EVIDENCE",
64
74
  "bucket_name": "domino-relay-evidence"
75
+ },
76
+ {
77
+ "binding": "HOSTING_ASSETS",
78
+ "bucket_name": "domino-relay-hosting-development"
65
79
  }
66
80
  ],
67
81
  "vars": {