@domino-sdk/relay-runtime 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/dist/index.js +1097 -194
  2. package/package.json +1 -1
  3. package/wrangler.jsonc +14 -0
package/dist/index.js CHANGED
@@ -14833,7 +14833,40 @@ var init_chunk_C2KOMNLG = __esm({
14833
14833
  }
14834
14834
  });
14835
14835
 
14836
- // packages/sdk/dist/chunk-NC3UAP6T.js
14836
+ // packages/sdk/dist/chunk-6IXQRMGW.js
14837
+ var authScopeSchema, principalSchema, sessionSchema, exchangeSchema, exchangeResultSchema;
14838
+ var init_chunk_6IXQRMGW = __esm({
14839
+ "packages/sdk/dist/chunk-6IXQRMGW.js"() {
14840
+ "use strict";
14841
+ init_chunk_C2KOMNLG();
14842
+ init_zod();
14843
+ authScopeSchema = external_exports.object({
14844
+ organization: identifier,
14845
+ project: identifier,
14846
+ environment: external_exports.enum(["test", "live"])
14847
+ }).strict();
14848
+ principalSchema = external_exports.discriminatedUnion("kind", [
14849
+ external_exports.object({ kind: external_exports.literal("member"), member: identifier }).strict(),
14850
+ external_exports.object({ kind: external_exports.literal("staff"), subject: identifier }).strict(),
14851
+ external_exports.object({ kind: external_exports.literal("operator"), subject: identifier }).strict()
14852
+ ]);
14853
+ sessionSchema = authScopeSchema.extend({
14854
+ id: identifier,
14855
+ principal: principalSchema,
14856
+ expiresAt: external_exports.number()
14857
+ });
14858
+ exchangeSchema = authScopeSchema.extend({
14859
+ identity: external_exports.object({ issuer: identifier, subject: external_exports.string().min(1).max(200) }).strict(),
14860
+ role: external_exports.enum(["member", "staff", "operator"])
14861
+ }).strict();
14862
+ exchangeResultSchema = external_exports.object({
14863
+ token: external_exports.string(),
14864
+ session: sessionSchema
14865
+ });
14866
+ }
14867
+ });
14868
+
14869
+ // packages/sdk/dist/chunk-L757OYXZ.js
14837
14870
  function questInteraction(trigger) {
14838
14871
  return trigger.kind === "automatic" ? "automatic" : trigger.actor === "staff" ? "staff" : trigger.input === "none" ? "claim" : trigger.input;
14839
14872
  }
@@ -15439,6 +15472,7 @@ function createRelayClient(options) {
15439
15472
  signOut: () => changeSession(
15440
15473
  () => request("/v1/auth/logout", external_exports.object({ ok: external_exports.boolean() }), json3({}))
15441
15474
  ),
15475
+ /** Local development only. Existing hosted apps use createParticipantBackend from /portal-proxy. */
15442
15476
  exchange: (input) => request("/v1/auth/exchange", exchangeResultSchema, json3(input))
15443
15477
  },
15444
15478
  me: {
@@ -15543,10 +15577,11 @@ function createRelayClient(options) {
15543
15577
  decide: (id2, input) => request(`/v1/attempts/${id2}/review`, attemptSchema, json3(input))
15544
15578
  };
15545
15579
  }
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;
15547
- var init_chunk_NC3UAP6T = __esm({
15548
- "packages/sdk/dist/chunk-NC3UAP6T.js"() {
15580
+ var interactionSchema, questTypeVersionSchema, collectionSlotSchema, questCollectionSchema, questInstanceSchema, questDraftChangesSchema, draftBase, questDraftSchema, questCatalogSchema, catalogDeploymentSchema, catalogDeploymentPreviewSchema, saveQuestDraftSchema, publishQuestDraftSchema, questViewSchema, collectionViewSchema, questExperienceSchema, questPublicationPreviewSchema, RelayError, common, claimInputSchema, draftSchema, recordSchema, QuestUpdatedError, solanaAddressSchema, walletConnectionSchema, walletChallengeInputSchema, walletChallengeSchema, walletProofSchema, solanaBalanceSchema, collectionCodeSchema, collectionPassSchema, collectionResolveSchema, collectionBeginSchema, collectionPreviewSchema, projectDeploymentSchema, deploymentPreviewSchema, publishDeploymentSchema, deploymentResultSchema, identityIntegrationInputSchema, identityIntegrationSchema, identityIntegrationSecretSchema, participantExchangeSchema;
15581
+ var init_chunk_L757OYXZ = __esm({
15582
+ "packages/sdk/dist/chunk-L757OYXZ.js"() {
15549
15583
  "use strict";
15584
+ init_chunk_6IXQRMGW();
15550
15585
  init_chunk_C2KOMNLG();
15551
15586
  init_zod();
15552
15587
  init_zod();
@@ -15718,29 +15753,6 @@ var init_chunk_NC3UAP6T = __esm({
15718
15753
  }
15719
15754
  draft;
15720
15755
  };
15721
- authScopeSchema = external_exports.object({
15722
- organization: identifier,
15723
- project: identifier,
15724
- environment: external_exports.enum(["test", "live"])
15725
- }).strict();
15726
- principalSchema = external_exports.discriminatedUnion("kind", [
15727
- external_exports.object({ kind: external_exports.literal("member"), member: identifier }).strict(),
15728
- external_exports.object({ kind: external_exports.literal("staff"), subject: identifier }).strict(),
15729
- external_exports.object({ kind: external_exports.literal("operator"), subject: identifier }).strict()
15730
- ]);
15731
- sessionSchema = authScopeSchema.extend({
15732
- id: identifier,
15733
- principal: principalSchema,
15734
- expiresAt: external_exports.number()
15735
- });
15736
- exchangeSchema = authScopeSchema.extend({
15737
- identity: external_exports.object({ issuer: identifier, subject: external_exports.string().min(1).max(200) }).strict(),
15738
- role: external_exports.enum(["member", "staff", "operator"])
15739
- }).strict();
15740
- exchangeResultSchema = external_exports.object({
15741
- token: external_exports.string(),
15742
- session: sessionSchema
15743
- });
15744
15756
  solanaAddressSchema = external_exports.string().min(32).max(44).regex(/^[1-9A-HJ-NP-Za-km-z]+$/);
15745
15757
  walletConnectionSchema = external_exports.object({
15746
15758
  chain: external_exports.literal("solana"),
@@ -15803,6 +15815,28 @@ var init_chunk_NC3UAP6T = __esm({
15803
15815
  deploymentResultSchema = deploymentPreviewSchema.extend({
15804
15816
  revision: external_exports.number().int().positive()
15805
15817
  });
15818
+ identityIntegrationInputSchema = external_exports.object({
15819
+ name: external_exports.string().trim().min(1).max(80),
15820
+ origin: external_exports.string().url().refine((value) => {
15821
+ const url2 = new URL(value);
15822
+ return url2.protocol === "https:" && url2.origin === value;
15823
+ }, "Use an exact HTTPS application origin")
15824
+ }).strict();
15825
+ identityIntegrationSchema = authScopeSchema.extend({
15826
+ id: external_exports.string().uuid(),
15827
+ name: external_exports.string(),
15828
+ origin: external_exports.string(),
15829
+ expiresAt: external_exports.number()
15830
+ });
15831
+ identityIntegrationSecretSchema = identityIntegrationSchema.extend(
15832
+ {
15833
+ token: external_exports.string()
15834
+ }
15835
+ );
15836
+ participantExchangeSchema = external_exports.object({
15837
+ subject: external_exports.string().min(1).max(200),
15838
+ previousToken: external_exports.string().max(200).optional()
15839
+ }).strict();
15806
15840
  }
15807
15841
  });
15808
15842
 
@@ -15839,12 +15873,16 @@ __export(dist_exports, {
15839
15873
  exchangeResultSchema: () => exchangeResultSchema,
15840
15874
  exchangeSchema: () => exchangeSchema,
15841
15875
  identifier: () => identifier,
15876
+ identityIntegrationInputSchema: () => identityIntegrationInputSchema,
15877
+ identityIntegrationSchema: () => identityIntegrationSchema,
15878
+ identityIntegrationSecretSchema: () => identityIntegrationSecretSchema,
15842
15879
  interactionSchema: () => interactionSchema,
15843
15880
  mediaUrlSchema: () => mediaUrlSchema,
15844
15881
  memberListSchema: () => memberListSchema,
15845
15882
  memberPageSchema: () => memberPageSchema,
15846
15883
  memberProgressSchema: () => memberProgressSchema,
15847
15884
  parseSettings: () => parseSettings,
15885
+ participantExchangeSchema: () => participantExchangeSchema,
15848
15886
  photoResultSchema: () => photoResultSchema,
15849
15887
  pickupAllocationSchema: () => pickupAllocationSchema,
15850
15888
  pickupAvailabilitySchema: () => pickupAvailabilitySchema,
@@ -15914,11 +15952,512 @@ __export(dist_exports, {
15914
15952
  var init_dist = __esm({
15915
15953
  "packages/sdk/dist/index.js"() {
15916
15954
  "use strict";
15917
- init_chunk_NC3UAP6T();
15955
+ init_chunk_L757OYXZ();
15956
+ init_chunk_6IXQRMGW();
15918
15957
  init_chunk_C2KOMNLG();
15919
15958
  }
15920
15959
  });
15921
15960
 
15961
+ // apps/api/src/identity-integration.ts
15962
+ init_zod();
15963
+ init_dist();
15964
+
15965
+ // apps/api/src/access-error.ts
15966
+ var AccessError = class extends Error {
15967
+ constructor(message2, status = 403) {
15968
+ super(message2);
15969
+ this.status = status;
15970
+ }
15971
+ };
15972
+
15973
+ // apps/api/src/auth-utils.ts
15974
+ function cookie(request, name) {
15975
+ return request.headers.get("Cookie")?.split(";").map((value) => value.trim()).find((value) => value.startsWith(name + "="))?.slice(name.length + 1) ?? "";
15976
+ }
15977
+ function bindingCookie(value, secure, name = "relay_auth_flow") {
15978
+ return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${value ? 600 : 0}${secure ? "; Secure" : ""}`;
15979
+ }
15980
+ async function hash2(value) {
15981
+ const bytes = await crypto.subtle.digest(
15982
+ "SHA-256",
15983
+ new TextEncoder().encode(value)
15984
+ );
15985
+ return Array.from(
15986
+ new Uint8Array(bytes),
15987
+ (b) => b.toString(16).padStart(2, "0")
15988
+ ).join("");
15989
+ }
15990
+ function sessionToken(request) {
15991
+ const bearer = request.headers.get("Authorization");
15992
+ if (bearer) return bearer.startsWith("Bearer ") ? bearer.slice(7) : "";
15993
+ return request.headers.get("Cookie")?.split(";").map((x) => x.trim()).find((x) => x.startsWith("relay_session="))?.slice("relay_session=".length) ?? "";
15994
+ }
15995
+ function sessionCookie(token, secure) {
15996
+ return `relay_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${token ? 28800 : 0}${secure ? "; Secure" : ""}`;
15997
+ }
15998
+ function browserMutationAllowed(request, env) {
15999
+ const origin = request.headers.get("Origin");
16000
+ return request.headers.get("X-Relay-CSRF") === "1" && !!origin && (env.AUTH_ORIGINS ?? "").split(",").map((value) => value.trim()).includes(origin);
16001
+ }
16002
+
16003
+ // apps/api/src/identity-integration.ts
16004
+ async function identityIntegrationRequest(request, env, participant) {
16005
+ const path = new URL(request.url).pathname;
16006
+ const key = request.headers.get("X-Relay-Integration-Key");
16007
+ const exchange = path === "/v1/auth/exchange" && !(env.LOCAL_DEVELOPMENT === "true" && ["localhost", "127.0.0.1", "[::1]"].includes(
16008
+ new URL(request.url).hostname
16009
+ ) && !request.headers.get("Authorization")?.startsWith("Bearer di_"));
16010
+ if (!exchange && !key) return null;
16011
+ try {
16012
+ const auth = env.AUTH.getByName("auth-v1");
16013
+ if (exchange) {
16014
+ if (request.method !== "POST")
16015
+ return Response.json(
16016
+ { error: "Use POST for identity exchange" },
16017
+ { status: 405 }
16018
+ );
16019
+ const result = await auth.exchangeParticipant(
16020
+ request.headers.get("Authorization")?.replace(/^Bearer /, "") ?? "",
16021
+ participantExchangeSchema.parse(await request.json())
16022
+ );
16023
+ if (result.kind === "denied")
16024
+ throw new AccessError(
16025
+ "Use a valid identity integration server key from Console Settings. CLI and management credentials cannot issue participant sessions.",
16026
+ 401
16027
+ );
16028
+ if (result.kind === "limited")
16029
+ throw new AccessError(
16030
+ "Too many identity exchange requests. Retry shortly.",
16031
+ 429
16032
+ );
16033
+ return Response.json(result.result);
16034
+ }
16035
+ const integration = await auth.verifyIntegration(key ?? "");
16036
+ if (!integration)
16037
+ throw new AccessError(
16038
+ "Identity integration key is invalid, expired, or revoked",
16039
+ 401
16040
+ );
16041
+ if (!/^\/v1\/(?:auth\/(?:session|logout|connections)|me\/(?:progress|experience|attempts|evidence|pass|collection-pass|wallet(?:\/challenge|\/verify)?)|attempts\/[\w-]+(?:\/retry)?)$/.test(
16042
+ path
16043
+ ))
16044
+ return Response.json(
16045
+ { error: "Route is not available through an identity integration" },
16046
+ { status: 404 }
16047
+ );
16048
+ if (request.headers.get("Origin") !== integration.origin)
16049
+ throw new AccessError(
16050
+ "Application origin does not match the identity integration",
16051
+ 403
16052
+ );
16053
+ const session = await auth.integrationSession(
16054
+ sessionToken(request),
16055
+ integration.id
16056
+ );
16057
+ if (!session)
16058
+ throw new AccessError(
16059
+ "Participant session does not belong to this identity integration",
16060
+ 401
16061
+ );
16062
+ const headers = new Headers(request.headers);
16063
+ headers.delete("X-Relay-Integration-Key");
16064
+ return participant(
16065
+ new Request(request, { headers }),
16066
+ {
16067
+ ...env,
16068
+ LOCAL_DEVELOPMENT: "false",
16069
+ TEST_HOOKS: "false",
16070
+ DEMO_AUTH: "false",
16071
+ AUTH_EMULATORS: void 0,
16072
+ API_TOKEN: void 0,
16073
+ AUTH_ORGANIZATION: integration.organization,
16074
+ AUTH_PROJECT: integration.project,
16075
+ AUTH_ENVIRONMENT: integration.environment,
16076
+ AUTH_ORIGINS: integration.origin
16077
+ },
16078
+ integration.id
16079
+ );
16080
+ } catch (error48) {
16081
+ if (error48 instanceof external_exports.ZodError || error48 instanceof SyntaxError)
16082
+ return Response.json(
16083
+ {
16084
+ code: "INVALID_IDENTITY_EXCHANGE",
16085
+ error: "Supply only a verified subject and optional previousToken"
16086
+ },
16087
+ { status: 400 }
16088
+ );
16089
+ if (error48 instanceof AccessError)
16090
+ return Response.json(
16091
+ { code: "IDENTITY_INTEGRATION_ACCESS_DENIED", error: error48.message },
16092
+ { status: error48.status }
16093
+ );
16094
+ throw error48;
16095
+ }
16096
+ }
16097
+
16098
+ // packages/sdk/dist/portal-proxy.js
16099
+ init_chunk_6IXQRMGW();
16100
+ init_chunk_C2KOMNLG();
16101
+ async function proxyParticipant(request, upstream) {
16102
+ const incoming = new URL(request.url);
16103
+ const path = incoming.pathname.slice("/relay".length);
16104
+ 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(
16105
+ path
16106
+ ) || /^\/v1\/me\/(progress|experience|attempts|evidence|pass|collection-pass|wallet(?:\/challenge|\/verify)?)$/.test(
16107
+ path
16108
+ ) || /^\/v1\/attempts\/[\w-]+(?:\/retry)?$/.test(path));
16109
+ if (!allowed || !["GET", "POST"].includes(request.method))
16110
+ return Response.json({ error: "Route not found" }, { status: 404 });
16111
+ const target = new URL(upstream.baseUrl);
16112
+ if (target.protocol !== "https:" || target.username || target.password || target.pathname !== "/" || target.search || target.hash)
16113
+ return Response.json(
16114
+ { error: "Participant API is not configured" },
16115
+ { status: 503 }
16116
+ );
16117
+ target.pathname = path;
16118
+ target.search = incoming.search;
16119
+ const headers = new Headers();
16120
+ for (const name of [
16121
+ "Cookie",
16122
+ "Origin",
16123
+ "Content-Type",
16124
+ "X-Relay-CSRF",
16125
+ "X-Relay-Session",
16126
+ "X-Relay-Upload-ID",
16127
+ "CF-Connecting-IP"
16128
+ ]) {
16129
+ const value = request.headers.get(name);
16130
+ if (value) headers.set(name, value);
16131
+ }
16132
+ const response = await upstream.fetch(target, {
16133
+ method: request.method,
16134
+ headers,
16135
+ body: request.method === "GET" ? void 0 : await request.arrayBuffer(),
16136
+ redirect: "manual"
16137
+ });
16138
+ const resultHeaders = new Headers(response.headers);
16139
+ resultHeaders.set("Cache-Control", "no-store");
16140
+ return new Response(response.body, {
16141
+ status: response.status,
16142
+ headers: resultHeaders
16143
+ });
16144
+ }
16145
+
16146
+ // apps/api/src/hosted-participant.ts
16147
+ init_zod();
16148
+ function hostedParticipantEnv(env, scope, origin) {
16149
+ return {
16150
+ ...env,
16151
+ LOCAL_DEVELOPMENT: "false",
16152
+ TEST_HOOKS: "false",
16153
+ DEMO_AUTH: "false",
16154
+ API_TOKEN: void 0,
16155
+ AUTH_EMULATORS: void 0,
16156
+ AUTH_ORGANIZATION: scope.organization,
16157
+ AUTH_PROJECT: scope.project,
16158
+ AUTH_ENVIRONMENT: "test",
16159
+ AUTH_ORIGINS: origin
16160
+ };
16161
+ }
16162
+ async function registeredStagingScope(origin, env) {
16163
+ const url2 = new URL(origin);
16164
+ if (url2.protocol !== "https:" || url2.origin !== origin || !env.STAGING_DOMAIN)
16165
+ return null;
16166
+ const suffix = `.${env.STAGING_DOMAIN}`;
16167
+ if (!url2.hostname.endsWith(suffix)) return null;
16168
+ const name = url2.hostname.slice(0, -suffix.length);
16169
+ if (!/^stage-[a-f0-9]{32}$/.test(name)) return null;
16170
+ const raw = name.slice(6);
16171
+ const id2 = `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`;
16172
+ const build = await env.MANAGEMENT.getByName("management-v1").activeBuild(id2);
16173
+ return build?.runtime === "hosted-test" ? { organization: build.organization, project: build.project } : null;
16174
+ }
16175
+ async function callbackEnvironment(request, env) {
16176
+ const url2 = new URL(request.url);
16177
+ if (request.method !== "GET") return env;
16178
+ const code = url2.pathname === "/v1/auth/authorize" ? url2.searchParams.get("code") : /^\/v1\/auth\/callback\/[a-z]+$/.test(url2.pathname) ? url2.searchParams.get("state") : null;
16179
+ if (!code) return env;
16180
+ const raw = await env.AUTH.getByName("auth-v1").readFlow(code);
16181
+ if (!raw) return env;
16182
+ const flow = external_exports.object({ origin: external_exports.string().url() }).safeParse(JSON.parse(raw));
16183
+ if (!flow.success) return env;
16184
+ const scope = await registeredStagingScope(flow.data.origin, env) ?? await registeredPreviewScope(flow.data.origin, env);
16185
+ return scope ? hostedParticipantEnv(env, scope, flow.data.origin) : env;
16186
+ }
16187
+
16188
+ // apps/api/src/development-preview.ts
16189
+ async function registeredPreviewScope(origin, env) {
16190
+ const url2 = new URL(origin);
16191
+ if (url2.protocol !== "https:" || url2.origin !== origin || !env.PREVIEW_DOMAIN || !env.PREVIEW_SESSIONS)
16192
+ return null;
16193
+ const suffix = `.${env.PREVIEW_DOMAIN}`;
16194
+ if (!url2.hostname.endsWith(suffix)) return null;
16195
+ const name = url2.hostname.slice(0, -suffix.length);
16196
+ if (!/^[a-z0-9-]+-[a-f0-9]{32}$/.test(name)) return null;
16197
+ const scope = await env.PREVIEW_SESSIONS.getByName(name.slice(-32)).resolve();
16198
+ return scope?.hostname === url2.hostname ? scope : null;
16199
+ }
16200
+ async function developmentPreview(request, env, participant) {
16201
+ const url2 = new URL(request.url);
16202
+ const scope = await registeredPreviewScope(url2.origin, env);
16203
+ if (!scope)
16204
+ return new Response(
16205
+ '<!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>',
16206
+ {
16207
+ status: 503,
16208
+ headers: {
16209
+ "Content-Type": "text/html; charset=utf-8",
16210
+ "Cache-Control": "no-store",
16211
+ "X-Robots-Tag": "noindex, nofollow"
16212
+ }
16213
+ }
16214
+ );
16215
+ if (url2.pathname.startsWith("/relay/"))
16216
+ return proxyParticipant(request, {
16217
+ baseUrl: "https://participant.internal",
16218
+ fetch: (input, init) => participant(
16219
+ new Request(input, init),
16220
+ hostedParticipantEnv(env, scope, url2.origin)
16221
+ )
16222
+ });
16223
+ if (url2.pathname === "/relay" || /^\/(management|__domino)(\/|$)/.test(url2.pathname))
16224
+ return new Response("Not found", { status: 404 });
16225
+ return new Response(null, {
16226
+ status: 204,
16227
+ headers: { "X-Domino-Preview-Origin": "allowed" }
16228
+ });
16229
+ }
16230
+
16231
+ // apps/api/src/preview-session.ts
16232
+ init_zod();
16233
+ import { DurableObject } from "cloudflare:workers";
16234
+
16235
+ // apps/api/src/preview-tunnel.ts
16236
+ init_zod();
16237
+ var PreviewTunnel = class {
16238
+ constructor(env) {
16239
+ this.env = env;
16240
+ }
16241
+ async call(path, method = "GET", body) {
16242
+ const response = await fetch(
16243
+ `https://api.cloudflare.com/client/v4${path}`,
16244
+ {
16245
+ method,
16246
+ headers: {
16247
+ Authorization: `Bearer ${this.env.PREVIEW_CF_TOKEN}`,
16248
+ "Content-Type": "application/json"
16249
+ },
16250
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
16251
+ signal: AbortSignal.timeout(1e4)
16252
+ }
16253
+ );
16254
+ if (response.status === 404 && method === "DELETE") return null;
16255
+ if (!response.ok)
16256
+ throw new Error(
16257
+ `Preview infrastructure request failed (${response.status}). Contact a Domino operator.`
16258
+ );
16259
+ const result = external_exports.object({ success: external_exports.boolean(), result: external_exports.unknown() }).parse(await response.json());
16260
+ if (!result.success)
16261
+ throw new Error(
16262
+ "Preview infrastructure request failed. Contact a Domino operator."
16263
+ );
16264
+ return result.result;
16265
+ }
16266
+ get base() {
16267
+ return `/accounts/${this.env.PREVIEW_CF_ACCOUNT}/cfd_tunnel`;
16268
+ }
16269
+ async lookup(name) {
16270
+ const tunnels = external_exports.array(external_exports.object({ id: external_exports.string().uuid() })).parse(
16271
+ await this.call(
16272
+ `${this.base}?name=${encodeURIComponent(name)}&is_deleted=false`
16273
+ )
16274
+ );
16275
+ return tunnels[0]?.id ?? null;
16276
+ }
16277
+ async ensure(name, hostname3, port) {
16278
+ const existing = external_exports.array(external_exports.object({ id: external_exports.string().uuid() })).parse(
16279
+ await this.call(
16280
+ `${this.base}?name=${encodeURIComponent(name)}&is_deleted=false`
16281
+ )
16282
+ );
16283
+ const tunnel = existing[0] ?? external_exports.object({ id: external_exports.string().uuid() }).parse(
16284
+ await this.call(this.base, "POST", {
16285
+ name,
16286
+ config_src: "cloudflare"
16287
+ })
16288
+ );
16289
+ await this.call(`${this.base}/${tunnel.id}/configurations`, "PUT", {
16290
+ config: {
16291
+ ingress: [
16292
+ {
16293
+ hostname: hostname3,
16294
+ service: `http://127.0.0.1:${port}`,
16295
+ originRequest: { httpHostHeader: hostname3 }
16296
+ },
16297
+ { service: "http_status:404" }
16298
+ ]
16299
+ }
16300
+ });
16301
+ const dnsBase = `/zones/${this.env.PREVIEW_CF_ZONE}/dns_records`;
16302
+ const records = external_exports.array(
16303
+ external_exports.object({ id: external_exports.string(), type: external_exports.string(), content: external_exports.string() })
16304
+ ).parse(
16305
+ await this.call(`${dnsBase}?name=${encodeURIComponent(hostname3)}`)
16306
+ );
16307
+ const content = `${tunnel.id}.cfargotunnel.com`;
16308
+ if (records.some(
16309
+ (record2) => record2.type !== "CNAME" || record2.content !== content
16310
+ ))
16311
+ throw new Error(
16312
+ "Preview hostname conflicts with an existing DNS record."
16313
+ );
16314
+ if (!records.length)
16315
+ await this.call(dnsBase, "POST", {
16316
+ type: "CNAME",
16317
+ name: hostname3,
16318
+ content,
16319
+ proxied: true,
16320
+ ttl: 1
16321
+ });
16322
+ return tunnel.id;
16323
+ }
16324
+ async token(id2) {
16325
+ return external_exports.string().min(1).parse(await this.call(`${this.base}/${id2}/token`));
16326
+ }
16327
+ async remove(id2, hostname3) {
16328
+ const base3 = `/zones/${this.env.PREVIEW_CF_ZONE}/dns_records`;
16329
+ const records = external_exports.array(
16330
+ external_exports.object({ id: external_exports.string(), type: external_exports.string(), content: external_exports.string() })
16331
+ ).parse(await this.call(`${base3}?name=${encodeURIComponent(hostname3)}`));
16332
+ for (const record2 of records) {
16333
+ if (record2.type !== "CNAME" || record2.content !== `${id2}.cfargotunnel.com`)
16334
+ throw new Error("Preview DNS ownership changed; cleanup stopped.");
16335
+ await this.call(`${base3}/${record2.id}`, "DELETE");
16336
+ }
16337
+ await this.call(`${this.base}/${id2}`, "DELETE");
16338
+ }
16339
+ async revoke(id2) {
16340
+ const secret = btoa(
16341
+ String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))
16342
+ );
16343
+ await this.call(`${this.base}/${id2}`, "PATCH", { tunnel_secret: secret });
16344
+ await this.call(`${this.base}/${id2}/connections`, "DELETE");
16345
+ }
16346
+ };
16347
+ function previewConfigurationIssues(env) {
16348
+ return Object.entries({
16349
+ PREVIEW_SESSIONS: env.PREVIEW_SESSIONS,
16350
+ PREVIEW_DOMAIN: env.PREVIEW_DOMAIN,
16351
+ PREVIEW_CF_TOKEN: env.PREVIEW_CF_TOKEN,
16352
+ PREVIEW_CF_ACCOUNT: env.PREVIEW_CF_ACCOUNT,
16353
+ PREVIEW_CF_ZONE: env.PREVIEW_CF_ZONE,
16354
+ PREVIEW_TLS_READY: env.PREVIEW_TLS_READY === "true"
16355
+ }).filter(([, value]) => !value).map(([key]) => key);
16356
+ }
16357
+
16358
+ // apps/api/src/preview-session.ts
16359
+ var sessionSchema2 = external_exports.object({
16360
+ organization: external_exports.string(),
16361
+ project: external_exports.string(),
16362
+ actor: external_exports.string(),
16363
+ hostname: external_exports.string(),
16364
+ generation: external_exports.string().uuid(),
16365
+ port: external_exports.number().int().min(1).max(65535),
16366
+ expiresAt: external_exports.number(),
16367
+ tunnel: external_exports.string().uuid().nullable()
16368
+ });
16369
+ var leaseMs = 9e4;
16370
+ var PreviewSession = class extends DurableObject {
16371
+ async read() {
16372
+ const value = await this.ctx.storage.get("session");
16373
+ return value ? sessionSchema2.parse(value) : null;
16374
+ }
16375
+ async resolve() {
16376
+ const session = await this.read();
16377
+ return session && session.expiresAt > Date.now() && session.tunnel ? {
16378
+ organization: session.organization,
16379
+ project: session.project,
16380
+ hostname: session.hostname
16381
+ } : null;
16382
+ }
16383
+ async start(input) {
16384
+ return this.ctx.blockConcurrencyWhile(async () => {
16385
+ const old = await this.read();
16386
+ if (old && old.expiresAt > Date.now() && old.generation !== input.generation)
16387
+ return {
16388
+ error: "This worktree already has a running preview. Stop it or wait 90 seconds for its lease to expire."
16389
+ };
16390
+ const provider = new PreviewTunnel(this.env);
16391
+ const next = {
16392
+ ...input,
16393
+ expiresAt: 0,
16394
+ tunnel: old?.tunnel ?? null
16395
+ };
16396
+ await this.ctx.storage.put("session", next);
16397
+ await this.ctx.storage.setAlarm(Date.now() + leaseMs);
16398
+ const tunnel = await provider.ensure(
16399
+ `domino-${input.hostname.split(".")[0]}`,
16400
+ input.hostname,
16401
+ input.port
16402
+ );
16403
+ if (!old || old.generation !== input.generation || old.expiresAt <= Date.now())
16404
+ await provider.revoke(tunnel);
16405
+ const token = await provider.token(tunnel);
16406
+ await this.ctx.storage.put("session", {
16407
+ ...next,
16408
+ tunnel,
16409
+ expiresAt: Date.now() + leaseMs
16410
+ });
16411
+ await this.ctx.storage.delete("retireAt");
16412
+ await this.ctx.storage.setAlarm(Date.now() + leaseMs);
16413
+ return { url: `https://${input.hostname}`, token, leaseMs };
16414
+ });
16415
+ }
16416
+ async heartbeat(generation) {
16417
+ return this.ctx.blockConcurrencyWhile(async () => {
16418
+ const session = await this.read();
16419
+ if (!session || session.generation !== generation || session.expiresAt <= Date.now())
16420
+ return false;
16421
+ session.expiresAt = Date.now() + leaseMs;
16422
+ await this.ctx.storage.put("session", session);
16423
+ await this.ctx.storage.setAlarm(session.expiresAt);
16424
+ return true;
16425
+ });
16426
+ }
16427
+ async stop(generation) {
16428
+ return this.ctx.blockConcurrencyWhile(async () => {
16429
+ const session = await this.read();
16430
+ if (!session || session.generation !== generation) return;
16431
+ await this.ctx.storage.put("session", { ...session, expiresAt: 0 });
16432
+ await this.ctx.storage.setAlarm(Date.now() + 1e3);
16433
+ });
16434
+ }
16435
+ async alarm() {
16436
+ await this.ctx.blockConcurrencyWhile(async () => {
16437
+ const session = await this.read();
16438
+ if (!session || session.expiresAt > Date.now()) return;
16439
+ const provider = new PreviewTunnel(this.env);
16440
+ const tunnel = session.tunnel ?? await provider.lookup(`domino-${session.hostname.split(".")[0]}`);
16441
+ if (!tunnel) return;
16442
+ await this.ctx.storage.put("session", { ...session, tunnel });
16443
+ await provider.revoke(tunnel);
16444
+ const retireAt = await this.ctx.storage.get("retireAt") ?? Date.now() + 7 * 864e5;
16445
+ if (retireAt <= Date.now()) {
16446
+ await provider.remove(tunnel, session.hostname);
16447
+ await this.ctx.storage.put("session", {
16448
+ ...session,
16449
+ tunnel: null,
16450
+ expiresAt: 0
16451
+ });
16452
+ await this.ctx.storage.delete("retireAt");
16453
+ } else {
16454
+ await this.ctx.storage.put("retireAt", retireAt);
16455
+ await this.ctx.storage.setAlarm(retireAt);
16456
+ }
16457
+ });
16458
+ }
16459
+ };
16460
+
15922
16461
  // apps/api/src/index.ts
15923
16462
  import { WorkerEntrypoint as WorkerEntrypoint3 } from "cloudflare:workers";
15924
16463
 
@@ -15932,6 +16471,7 @@ init_zod();
15932
16471
  init_zod();
15933
16472
  init_dist();
15934
16473
  init_dist();
16474
+ init_zod();
15935
16475
  var repositorySchema = external_exports.object({
15936
16476
  id: external_exports.string().uuid(),
15937
16477
  organization: identifier,
@@ -15960,6 +16500,7 @@ var stageRequestSchema = external_exports.object({
15960
16500
  requestId: external_exports.string().uuid()
15961
16501
  }).strict();
15962
16502
  var buildIdentitySchema = external_exports.object({
16503
+ runtime: external_exports.enum(["simulation", "hosted-test"]).default("simulation"),
15963
16504
  id: external_exports.string().uuid(),
15964
16505
  repository: external_exports.string().uuid(),
15965
16506
  organization: identifier,
@@ -16011,6 +16552,30 @@ var buildArtifactsSchema = external_exports.object({
16011
16552
  if (value.assets.reduce((sum, a) => sum + a.size, 0) > 1e7)
16012
16553
  ctx.addIssue({ code: "custom", message: "Staging assets exceed 10 MB" });
16013
16554
  });
16555
+ var developmentPreflightSchema = external_exports.object({
16556
+ organization: external_exports.string(),
16557
+ project: external_exports.string(),
16558
+ environment: external_exports.enum(["test", "live"]),
16559
+ identity: external_exports.object({
16560
+ exchange: external_exports.literal("supported"),
16561
+ activeIntegrations: external_exports.number().int().nonnegative()
16562
+ }).optional(),
16563
+ staging: external_exports.enum(["configured", "blocked"]),
16564
+ development: external_exports.object({
16565
+ status: external_exports.enum(["configured", "blocked"]),
16566
+ missing: external_exports.array(external_exports.string()),
16567
+ authentication: external_exports.array(external_exports.string())
16568
+ }),
16569
+ checks: external_exports.array(
16570
+ external_exports.object({
16571
+ id: external_exports.string(),
16572
+ status: external_exports.enum(["ok", "error"]),
16573
+ message: external_exports.string(),
16574
+ missing: external_exports.array(external_exports.string()).optional(),
16575
+ issues: external_exports.array(external_exports.string()).optional()
16576
+ })
16577
+ )
16578
+ });
16014
16579
  var environmentSchema = external_exports.enum(["test", "live"]);
16015
16580
  var roleSchema = external_exports.enum(["editor", "reviewer", "staff"]);
16016
16581
  var permissionSchema = external_exports.enum([
@@ -16027,7 +16592,12 @@ var rolePermissions = {
16027
16592
  reviewer: ["read", "review"],
16028
16593
  staff: ["read", "confirm", "handover"]
16029
16594
  };
16595
+ var projectSetupSchema = external_exports.object({
16596
+ mode: external_exports.enum(["new", "existing"]),
16597
+ brief: external_exports.string().trim().min(1).max(4e3)
16598
+ }).strict();
16030
16599
  var projectSchema = external_exports.object({
16600
+ setup: projectSetupSchema.optional(),
16031
16601
  id: external_exports.string().min(1),
16032
16602
  organization: identifier,
16033
16603
  project: identifier,
@@ -16038,7 +16608,11 @@ var projectSchema = external_exports.object({
16038
16608
  permissions: external_exports.array(permissionSchema),
16039
16609
  environments: external_exports.array(environmentSchema)
16040
16610
  });
16041
- var projectInputSchema = external_exports.object({ project: identifier, name: external_exports.string().trim().min(1).max(80) }).strict();
16611
+ var projectInputSchema = external_exports.object({
16612
+ project: identifier,
16613
+ name: external_exports.string().trim().min(1).max(80),
16614
+ setup: projectSetupSchema.optional()
16615
+ }).strict();
16042
16616
  var grantSchema = external_exports.object({
16043
16617
  subject: external_exports.string().min(1).max(200),
16044
16618
  roles: external_exports.array(roleSchema),
@@ -16145,87 +16719,41 @@ var memberDetailSchema = external_exports.object({
16145
16719
  member: memberSchema,
16146
16720
  progress: memberProgressSchema,
16147
16721
  passes: external_exports.array(
16148
- external_exports.object({
16149
- provider: external_exports.string(),
16150
- event: external_exports.string(),
16151
- subject: external_exports.string(),
16152
- attempt: identifier.nullable()
16153
- })
16154
- ).default([]),
16155
- wallet: external_exports.object({
16156
- connection: walletConnectionSchema.nullable(),
16157
- observation: external_exports.object({ attempt: identifier, balance: solanaBalanceSchema }).nullable()
16158
- }).default({ connection: null, observation: null })
16159
- });
16160
- var stockInputSchema = external_exports.object({
16161
- item: identifier,
16162
- quantity: external_exports.number().int().positive().max(1e6),
16163
- actionId: identifier
16164
- }).strict();
16165
- var tokenInputSchema = external_exports.object({
16166
- name: external_exports.string().trim().min(1).max(80),
16167
- project: identifier,
16168
- environment: environmentSchema,
16169
- permissions: external_exports.array(permissionSchema).min(1),
16170
- days: external_exports.number().int().min(1).max(90).default(30)
16171
- }).strict();
16172
- var tokenSchema = external_exports.object({
16173
- id: external_exports.string(),
16174
- name: external_exports.string(),
16175
- project: external_exports.string(),
16176
- environment: environmentSchema,
16177
- permissions: external_exports.array(permissionSchema),
16178
- expiresAt: external_exports.number()
16179
- });
16180
-
16181
- // apps/api/src/staging.ts
16182
- init_dist();
16183
-
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
- }
16722
+ external_exports.object({
16723
+ provider: external_exports.string(),
16724
+ event: external_exports.string(),
16725
+ subject: external_exports.string(),
16726
+ attempt: identifier.nullable()
16727
+ })
16728
+ ).default([]),
16729
+ wallet: external_exports.object({
16730
+ connection: walletConnectionSchema.nullable(),
16731
+ observation: external_exports.object({ attempt: identifier, balance: solanaBalanceSchema }).nullable()
16732
+ }).default({ connection: null, observation: null })
16733
+ });
16734
+ var stockInputSchema = external_exports.object({
16735
+ item: identifier,
16736
+ quantity: external_exports.number().int().positive().max(1e6),
16737
+ actionId: identifier
16738
+ }).strict();
16739
+ var tokenInputSchema = external_exports.object({
16740
+ name: external_exports.string().trim().min(1).max(80),
16741
+ project: identifier,
16742
+ environment: environmentSchema,
16743
+ permissions: external_exports.array(permissionSchema).min(1),
16744
+ days: external_exports.number().int().min(1).max(90).default(30)
16745
+ }).strict();
16746
+ var tokenSchema = external_exports.object({
16747
+ id: external_exports.string(),
16748
+ name: external_exports.string(),
16749
+ project: external_exports.string(),
16750
+ environment: environmentSchema,
16751
+ permissions: external_exports.array(permissionSchema),
16752
+ expiresAt: external_exports.number()
16753
+ });
16754
+
16755
+ // apps/api/src/staging.ts
16756
+ init_dist();
16229
16757
 
16230
16758
  // apps/api/src/quest-catalog.ts
16231
16759
  init_dist();
@@ -17048,7 +17576,7 @@ var ManagementReads = class {
17048
17576
  // apps/api/src/ledger.ts
17049
17577
  init_zod();
17050
17578
  init_dist();
17051
- import { DurableObject } from "cloudflare:workers";
17579
+ import { DurableObject as DurableObject2 } from "cloudflare:workers";
17052
17580
 
17053
17581
  // apps/api/src/quest-graph.ts
17054
17582
  init_dist();
@@ -17222,36 +17750,6 @@ function pageQuery(params) {
17222
17750
  return entries;
17223
17751
  }
17224
17752
 
17225
- // apps/api/src/auth-utils.ts
17226
- function cookie(request, name) {
17227
- return request.headers.get("Cookie")?.split(";").map((value) => value.trim()).find((value) => value.startsWith(name + "="))?.slice(name.length + 1) ?? "";
17228
- }
17229
- function bindingCookie(value, secure, name = "relay_auth_flow") {
17230
- return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${value ? 600 : 0}${secure ? "; Secure" : ""}`;
17231
- }
17232
- async function hash2(value) {
17233
- const bytes = await crypto.subtle.digest(
17234
- "SHA-256",
17235
- new TextEncoder().encode(value)
17236
- );
17237
- return Array.from(
17238
- new Uint8Array(bytes),
17239
- (b) => b.toString(16).padStart(2, "0")
17240
- ).join("");
17241
- }
17242
- function sessionToken(request) {
17243
- const bearer = request.headers.get("Authorization");
17244
- if (bearer) return bearer.startsWith("Bearer ") ? bearer.slice(7) : "";
17245
- return request.headers.get("Cookie")?.split(";").map((x) => x.trim()).find((x) => x.startsWith("relay_session="))?.slice("relay_session=".length) ?? "";
17246
- }
17247
- function sessionCookie(token, secure) {
17248
- return `relay_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${token ? 28800 : 0}${secure ? "; Secure" : ""}`;
17249
- }
17250
- function browserMutationAllowed(request, env) {
17251
- const origin = request.headers.get("Origin");
17252
- return request.headers.get("X-Relay-CSRF") === "1" && !!origin && (env.AUTH_ORIGINS ?? "").split(",").map((value) => value.trim()).includes(origin);
17253
- }
17254
-
17255
17753
  // node_modules/.pnpm/base-x@5.0.1/node_modules/base-x/src/esm/index.js
17256
17754
  function base2(ALPHABET2) {
17257
17755
  if (ALPHABET2.length >= 255) {
@@ -17507,7 +18005,7 @@ function json2(value) {
17507
18005
  function parse3(schema, data) {
17508
18006
  return schema.parse(JSON.parse(data));
17509
18007
  }
17510
- var OrganizationLedger = class extends DurableObject {
18008
+ var OrganizationLedger = class extends DurableObject2 {
17511
18009
  sql;
17512
18010
  management;
17513
18011
  catalog;
@@ -19411,6 +19909,9 @@ var OrganizationLedger = class extends DurableObject {
19411
19909
  }
19412
19910
  };
19413
19911
 
19912
+ // apps/api/src/auth.ts
19913
+ init_dist();
19914
+
19414
19915
  // apps/api/src/account-profile.ts
19415
19916
  init_zod();
19416
19917
 
@@ -20541,11 +21042,14 @@ async function profileFromStoredGrant(env, sealed, subject) {
20541
21042
 
20542
21043
  // apps/api/src/auth.ts
20543
21044
  init_dist();
20544
- import { DurableObject as DurableObject2 } from "cloudflare:workers";
20545
- var AuthStore = class extends DurableObject2 {
21045
+ import { DurableObject as DurableObject3 } from "cloudflare:workers";
21046
+ var AuthStore = class extends DurableObject3 {
20546
21047
  constructor(ctx, env) {
20547
21048
  super(ctx, env);
20548
21049
  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));
21050
+ CREATE TABLE IF NOT EXISTS identity_integrations (id TEXT PRIMARY KEY, secret TEXT NOT NULL, data TEXT NOT NULL);
21051
+ CREATE TABLE IF NOT EXISTS integration_sessions (hash TEXT PRIMARY KEY, integration TEXT NOT NULL, subject TEXT NOT NULL);
21052
+ CREATE TABLE IF NOT EXISTS integration_audit (actor TEXT NOT NULL, action TEXT NOT NULL, integration TEXT NOT NULL, at INTEGER NOT NULL);
20549
21053
  CREATE TABLE IF NOT EXISTS sessions (hash TEXT PRIMARY KEY, expires INTEGER NOT NULL, data TEXT NOT NULL);
20550
21054
  CREATE TABLE IF NOT EXISTS auth_flows (id TEXT PRIMARY KEY, expires INTEGER NOT NULL, data TEXT NOT NULL);
20551
21055
  CREATE TABLE IF NOT EXISTS connections (scope TEXT NOT NULL, issuer TEXT NOT NULL, subject TEXT NOT NULL, member TEXT NOT NULL, login INTEGER NOT NULL, provider TEXT NOT NULL, sealed TEXT NOT NULL, PRIMARY KEY(scope,issuer,subject));
@@ -20554,6 +21058,134 @@ var AuthStore = class extends DurableObject2 {
20554
21058
  CREATE TABLE IF NOT EXISTS wallet_challenges (id TEXT PRIMARY KEY, expires INTEGER NOT NULL, data TEXT NOT NULL, result TEXT);
20555
21059
  CREATE TABLE IF NOT EXISTS wallet_connections (scope TEXT NOT NULL, project TEXT NOT NULL, member TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY(scope,project,member));`);
20556
21060
  }
21061
+ integrations(scope) {
21062
+ return this.ctx.storage.sql.exec("SELECT data FROM identity_integrations").toArray().map((row) => identityIntegrationSchema.parse(JSON.parse(row.data))).filter(
21063
+ (item) => item.organization === scope.organization && item.project === scope.project && item.environment === scope.environment
21064
+ );
21065
+ }
21066
+ async createIntegration(scope, input, actor) {
21067
+ const record2 = identityIntegrationSchema.parse({
21068
+ ...authScopeSchema.parse(scope),
21069
+ ...identityIntegrationInputSchema.parse(input),
21070
+ id: crypto.randomUUID(),
21071
+ expiresAt: Date.now() + 90 * 864e5
21072
+ });
21073
+ const token = "di_" + crypto.randomUUID() + crypto.randomUUID();
21074
+ const digest = await hash2(token);
21075
+ this.ctx.storage.transactionSync(() => {
21076
+ this.ctx.storage.sql.exec(
21077
+ "INSERT INTO identity_integrations VALUES(?,?,?)",
21078
+ record2.id,
21079
+ digest,
21080
+ JSON.stringify(record2)
21081
+ );
21082
+ this.ctx.storage.sql.exec(
21083
+ "INSERT INTO integration_audit VALUES(?,?,?,?)",
21084
+ actor,
21085
+ "created",
21086
+ record2.id,
21087
+ Date.now()
21088
+ );
21089
+ });
21090
+ return { ...record2, token };
21091
+ }
21092
+ async changeIntegration(scope, id2, actor, rotate) {
21093
+ const token = "di_" + crypto.randomUUID() + crypto.randomUUID();
21094
+ const digest = await hash2(token);
21095
+ return this.ctx.storage.transactionSync(() => {
21096
+ const record2 = this.integrations(scope).find((item) => item.id === id2);
21097
+ if (!record2) return null;
21098
+ this.ctx.storage.sql.exec(
21099
+ "INSERT INTO integration_audit VALUES(?,?,?,?)",
21100
+ actor,
21101
+ rotate ? "rotated" : "revoked",
21102
+ id2,
21103
+ Date.now()
21104
+ );
21105
+ if (rotate) {
21106
+ const next = { ...record2, expiresAt: Date.now() + 90 * 864e5 };
21107
+ this.ctx.storage.sql.exec(
21108
+ "UPDATE identity_integrations SET secret=?,data=? WHERE id=?",
21109
+ digest,
21110
+ JSON.stringify(next),
21111
+ id2
21112
+ );
21113
+ return { ...next, token };
21114
+ }
21115
+ this.ctx.storage.sql.exec(
21116
+ "DELETE FROM sessions WHERE hash IN (SELECT hash FROM integration_sessions WHERE integration=?)",
21117
+ id2
21118
+ );
21119
+ this.ctx.storage.sql.exec(
21120
+ "DELETE FROM integration_sessions WHERE integration=?",
21121
+ id2
21122
+ );
21123
+ this.ctx.storage.sql.exec(
21124
+ "DELETE FROM identity_integrations WHERE id=?",
21125
+ id2
21126
+ );
21127
+ return null;
21128
+ });
21129
+ }
21130
+ async verifyIntegration(token) {
21131
+ const row = this.ctx.storage.sql.exec(
21132
+ "SELECT data FROM identity_integrations WHERE secret=?",
21133
+ await hash2(token)
21134
+ ).toArray()[0];
21135
+ const record2 = row ? identityIntegrationSchema.parse(JSON.parse(row.data)) : null;
21136
+ return record2 && record2.expiresAt > Date.now() ? record2 : null;
21137
+ }
21138
+ async integrationSession(token, id2, subject) {
21139
+ const digest = await hash2(token);
21140
+ const row = this.ctx.storage.sql.exec(
21141
+ "SELECT integration,subject FROM integration_sessions WHERE hash=?",
21142
+ digest
21143
+ ).toArray()[0];
21144
+ if (!row || row.integration !== id2 || subject !== void 0 && row.subject !== subject)
21145
+ return null;
21146
+ return this.resolve(token, id2);
21147
+ }
21148
+ async exchangeParticipant(secret, value) {
21149
+ const input = participantExchangeSchema.parse(value);
21150
+ const record2 = await this.verifyIntegration(secret);
21151
+ if (!record2) return { kind: "denied" };
21152
+ if (input.previousToken) {
21153
+ const session = await this.integrationSession(
21154
+ input.previousToken,
21155
+ record2.id,
21156
+ input.subject
21157
+ );
21158
+ if (session)
21159
+ return {
21160
+ kind: "issued",
21161
+ result: { token: input.previousToken, session }
21162
+ };
21163
+ }
21164
+ if (!this.rateLimit("integration:" + record2.id, 600, 6e4))
21165
+ return { kind: "limited" };
21166
+ const result = await this.issue({
21167
+ organization: record2.organization,
21168
+ project: record2.project,
21169
+ environment: record2.environment,
21170
+ identity: { issuer: "app_" + record2.id, subject: input.subject },
21171
+ role: "member"
21172
+ });
21173
+ if (!await this.verifyIntegration(secret)) {
21174
+ await this.revoke(result.token);
21175
+ return { kind: "denied" };
21176
+ }
21177
+ const digest = await hash2(result.token);
21178
+ this.ctx.storage.sql.exec(
21179
+ "DELETE FROM integration_sessions WHERE hash NOT IN (SELECT hash FROM sessions)"
21180
+ );
21181
+ this.ctx.storage.sql.exec(
21182
+ "INSERT INTO integration_sessions VALUES(?,?,?)",
21183
+ digest,
21184
+ record2.id,
21185
+ input.subject
21186
+ );
21187
+ return { kind: "issued", result };
21188
+ }
20557
21189
  walletConnection(session) {
20558
21190
  if (session.principal.kind !== "member") return null;
20559
21191
  const row = this.ctx.storage.sql.exec(
@@ -20917,13 +21549,21 @@ var AuthStore = class extends DurableObject2 {
20917
21549
  signInEnabled: !!row.login
20918
21550
  }));
20919
21551
  }
20920
- async resolve(token) {
21552
+ async resolve(token, integrationId) {
21553
+ const digest = await hash2(token);
20921
21554
  const row = this.ctx.storage.sql.exec(
20922
21555
  "SELECT data FROM sessions WHERE hash=? AND expires>?",
20923
- await hash2(token),
21556
+ digest,
20924
21557
  Date.now()
20925
21558
  ).toArray()[0];
20926
- return row ? sessionSchema.parse(JSON.parse(row.data)) : null;
21559
+ if (!row) return null;
21560
+ const binding = this.ctx.storage.sql.exec(
21561
+ "SELECT i.data,s.integration FROM integration_sessions s LEFT JOIN identity_integrations i ON i.id=s.integration WHERE s.hash=?",
21562
+ digest
21563
+ ).toArray()[0];
21564
+ if (binding && (binding.integration !== integrationId || !binding.data || identityIntegrationSchema.parse(JSON.parse(binding.data)).expiresAt <= Date.now()))
21565
+ return null;
21566
+ return sessionSchema.parse(JSON.parse(row.data));
20927
21567
  }
20928
21568
  async revoke(token) {
20929
21569
  this.ctx.storage.sql.exec(
@@ -20935,16 +21575,6 @@ var AuthStore = class extends DurableObject2 {
20935
21575
 
20936
21576
  // apps/api/src/github-repositories.ts
20937
21577
  init_zod();
20938
-
20939
- // apps/api/src/access-error.ts
20940
- var AccessError = class extends Error {
20941
- constructor(message2, status = 403) {
20942
- super(message2);
20943
- this.status = status;
20944
- }
20945
- };
20946
-
20947
- // apps/api/src/github-repositories.ts
20948
21578
  var githubRepositorySchema = external_exports.object({
20949
21579
  kind: external_exports.literal("github"),
20950
21580
  owner: external_exports.string().regex(/^[A-Za-z0-9-]+$/),
@@ -21195,33 +21825,32 @@ var HostingControl = class extends WorkerEntrypoint2 {
21195
21825
  const artifacts = buildArtifactsSchema.parse(await object2.json());
21196
21826
  const releases = batchReleaseSchema.parse(artifacts.quests);
21197
21827
  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)])
21828
+ const ledger = (build.runtime === "hosted-test" ? this.env.LEDGER : this.env.STAGING_LEDGER).getByName(
21829
+ JSON.stringify([
21830
+ build.organization,
21831
+ "test",
21832
+ build.runtime === "hosted-test" ? build.project : previewProject(build.id)
21833
+ ])
21200
21834
  );
21201
21835
  const headers = {
21202
21836
  "Content-Type": "application/json",
21203
21837
  "X-Relay-Actor": "management",
21204
21838
  "X-Relay-Actor-ID": build.actor
21205
21839
  };
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 })
21840
+ const preview = await ledger.fetch(
21841
+ new Request("https://ledger/management/v1/deployments/preview", {
21842
+ method: "POST",
21843
+ headers,
21844
+ body: JSON.stringify({ ...releases, ...catalog })
21845
+ })
21218
21846
  );
21219
- const { revision } = external_exports.object({ revision: external_exports.number() }).parse(await current.json());
21847
+ if (!preview.ok) throw new Error(await preview.text());
21848
+ const { id: id2 } = external_exports.object({ id: external_exports.string() }).parse(await preview.json());
21220
21849
  const deployed = await ledger.fetch(
21221
- new Request("https://ledger/management/v1/catalog/deploy", {
21850
+ new Request("https://ledger/management/v1/deployments/publish", {
21222
21851
  method: "POST",
21223
21852
  headers,
21224
- body: JSON.stringify({ ...catalog, expectedRevision: revision })
21853
+ body: JSON.stringify({ id: id2 })
21225
21854
  })
21226
21855
  );
21227
21856
  if (!deployed.ok) throw new Error(await deployed.text());
@@ -21250,6 +21879,15 @@ async function stagingRequest(request, env, participant) {
21250
21879
  if (!build || !env.STAGING_APPS || !env.STAGING_AUTH || !env.STAGING_LEDGER)
21251
21880
  return new Response("No staging build is ready yet", { status: 404 });
21252
21881
  if (url2.pathname.startsWith("/relay/")) {
21882
+ if (build.runtime === "hosted-test") {
21883
+ return proxyParticipant(request, {
21884
+ baseUrl: "https://participant.internal",
21885
+ fetch: (input, init) => participant(
21886
+ new Request(input, init),
21887
+ hostedParticipantEnv(env, build, url2.origin)
21888
+ )
21889
+ });
21890
+ }
21253
21891
  if (url2.pathname.startsWith("/relay/v1/auth/") && !["/relay/v1/auth/session", "/relay/v1/auth/logout"].includes(
21254
21892
  url2.pathname
21255
21893
  ))
@@ -21332,7 +21970,7 @@ async function stagingRequest(request, env, participant) {
21332
21970
  return response.headers.get("Content-Type")?.includes("text/html") ? new HTMLRewriter().on("body", {
21333
21971
  element(element) {
21334
21972
  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>',
21973
+ '<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
21974
  { html: true }
21337
21975
  );
21338
21976
  }
@@ -22594,6 +23232,27 @@ function localParticipantRequest(request, env) {
22594
23232
  return env.LOCAL_DEVELOPMENT === "true" && ["localhost", "127.0.0.1", "[::1]"].includes(new URL(request.url).hostname);
22595
23233
  }
22596
23234
 
23235
+ // apps/api/src/management.ts
23236
+ init_dist();
23237
+
23238
+ // apps/api/src/development-preflight.ts
23239
+ function stagingConfigurationIssues(env) {
23240
+ const required2 = {
23241
+ BUILDER: env.BUILDER,
23242
+ HOSTING_ASSETS: env.HOSTING_ASSETS,
23243
+ STAGING_DOMAIN: env.STAGING_DOMAIN,
23244
+ STAGING_APPS: env.STAGING_APPS,
23245
+ STAGING_AUTH: env.STAGING_AUTH,
23246
+ STAGING_LEDGER: env.STAGING_LEDGER,
23247
+ GITHUB_APP_ID: env.GITHUB_APP_ID,
23248
+ GITHUB_APP_PRIVATE_KEY: env.GITHUB_APP_PRIVATE_KEY,
23249
+ GITHUB_INSTALLATION_ID: env.GITHUB_INSTALLATION_ID,
23250
+ GITHUB_REPOSITORY_OWNER: env.GITHUB_REPOSITORY_OWNER,
23251
+ GIT_PUBLIC_URL: env.GIT_PUBLIC_URL
23252
+ };
23253
+ return Object.entries(required2).filter(([, value]) => !value).map(([key]) => key);
23254
+ }
23255
+
22597
23256
  // apps/api/src/readiness.ts
22598
23257
  function launchReadiness(env, releases, scope) {
22599
23258
  const issues = productionParticipantIssues(env);
@@ -32352,6 +33011,172 @@ async function managementRoutes(request, env) {
32352
33011
  }
32353
33012
  if (!organization || !(await store.projects(organization)).some((p) => p.project === project))
32354
33013
  throw new AccessError("Project not found", 404);
33014
+ if (path === "/identity-integrations" || path.startsWith("/identity-integrations/")) {
33015
+ if (personal || cliSession || !admin)
33016
+ throw new AccessError(
33017
+ "An organization administrator must manage identity integrations in Console"
33018
+ );
33019
+ const auth = env.AUTH.getByName("auth-v1");
33020
+ const scope = { organization, project, environment };
33021
+ if (path === "/identity-integrations" && request.method === "GET")
33022
+ return respond(await auth.integrations(scope));
33023
+ if (path === "/identity-integrations" && request.method === "POST")
33024
+ return respond(
33025
+ await auth.createIntegration(
33026
+ scope,
33027
+ identityIntegrationInputSchema.parse(await request.json()),
33028
+ actor
33029
+ ),
33030
+ 201
33031
+ );
33032
+ const match2 = path.match(
33033
+ /^\/identity-integrations\/([\w-]+)(\/rotate)?$/
33034
+ );
33035
+ if (match2 && (match2[2] && request.method === "POST" || !match2[2] && request.method === "DELETE")) {
33036
+ return respond(
33037
+ await auth.changeIntegration(
33038
+ scope,
33039
+ external_exports.string().uuid().parse(match2[1]),
33040
+ actor,
33041
+ !!match2[2]
33042
+ )
33043
+ );
33044
+ }
33045
+ throw new AccessError("Identity integration route not found", 404);
33046
+ }
33047
+ if (path === "/development/session" && ["POST", "PUT", "DELETE"].includes(request.method)) {
33048
+ requirePermission("publish");
33049
+ if (environment !== "test")
33050
+ throw new AccessError("Development requires the test environment", 422);
33051
+ if (previewConfigurationIssues(env).length || !env.PREVIEW_SESSIONS || !env.PREVIEW_DOMAIN)
33052
+ throw new AccessError(
33053
+ "Shared development previews are not configured. A Domino operator must enable preview hosting. Use --offline only for isolated platform development.",
33054
+ 503
33055
+ );
33056
+ const input = external_exports.object({
33057
+ workspace: external_exports.string().uuid(),
33058
+ generation: external_exports.string().uuid(),
33059
+ port: external_exports.number().int().min(1).max(65535).optional()
33060
+ }).strict().parse(await request.json());
33061
+ const id2 = (await hash2(
33062
+ JSON.stringify([organization, project, actor, input.workspace])
33063
+ )).slice(0, 32);
33064
+ const session = env.PREVIEW_SESSIONS.getByName(id2);
33065
+ if (request.method === "DELETE") {
33066
+ await session.stop(input.generation);
33067
+ return respond({ stopped: true });
33068
+ }
33069
+ if (request.method === "PUT") {
33070
+ if (!await session.heartbeat(input.generation))
33071
+ throw new AccessError(
33072
+ "Preview lease expired. Restart domino dev.",
33073
+ 409
33074
+ );
33075
+ return respond({ renewed: true });
33076
+ }
33077
+ if (!input.port) throw new AccessError("An app port is required", 400);
33078
+ if (!await env.AUTH.getByName("auth-v1").rateLimit(
33079
+ `preview-start:${organization}:${actor}`,
33080
+ 10,
33081
+ 6e4
33082
+ ))
33083
+ throw new AccessError(
33084
+ "Too many preview starts. Try again shortly.",
33085
+ 429
33086
+ );
33087
+ const hostname3 = `${project.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 24)}-${id2}.${env.PREVIEW_DOMAIN}`;
33088
+ const issues = productionParticipantIssues(
33089
+ hostedParticipantEnv(
33090
+ env,
33091
+ { organization, project },
33092
+ `https://${hostname3}`
33093
+ )
33094
+ );
33095
+ if (issues.length)
33096
+ throw new AccessError(
33097
+ "Hosted test sign-in is not configured. A Domino operator must enable participant authentication.",
33098
+ 503
33099
+ );
33100
+ const result = await session.start({
33101
+ organization,
33102
+ project,
33103
+ actor,
33104
+ hostname: hostname3,
33105
+ generation: input.generation,
33106
+ port: input.port
33107
+ });
33108
+ if ("error" in result)
33109
+ throw new AccessError(
33110
+ result.error ?? "Preview is already running",
33111
+ 409
33112
+ );
33113
+ return respond(result);
33114
+ }
33115
+ if (path === "/preflight" && request.method === "GET") {
33116
+ requirePermission("read");
33117
+ const missing = stagingConfigurationIssues(env);
33118
+ const repository = await store.repository(organization, project);
33119
+ const authIssues = productionParticipantIssues(
33120
+ hostedParticipantEnv(
33121
+ env,
33122
+ { organization, project },
33123
+ `https://preflight.${env.PREVIEW_DOMAIN ?? "preview.domino.run"}`
33124
+ )
33125
+ );
33126
+ const checks = [
33127
+ {
33128
+ id: "hosted-test-auth",
33129
+ status: authIssues.length ? "error" : "ok",
33130
+ 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.",
33131
+ issues: authIssues
33132
+ },
33133
+ {
33134
+ id: "project-access",
33135
+ status: "ok",
33136
+ message: "Project access verified."
33137
+ },
33138
+ {
33139
+ id: "publication",
33140
+ status: permissions.includes("publish") ? "ok" : "error",
33141
+ message: permissions.includes("publish") ? "Publication permission verified." : "Ask a project administrator for publication permission."
33142
+ },
33143
+ {
33144
+ id: "cloud-builds",
33145
+ status: missing.length ? "error" : "ok",
33146
+ 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.",
33147
+ missing
33148
+ },
33149
+ {
33150
+ id: "repository",
33151
+ status: repository?.status === "ready" ? "ok" : "error",
33152
+ message: repository?.status === "ready" ? "Hosted repository is ready." : "Create a hosted repository before staging."
33153
+ }
33154
+ ];
33155
+ const integrations = await env.AUTH.getByName("auth-v1").integrations({
33156
+ organization,
33157
+ project,
33158
+ environment
33159
+ });
33160
+ const developmentMissing = previewConfigurationIssues(env);
33161
+ return respond({
33162
+ organization,
33163
+ project,
33164
+ environment,
33165
+ identity: {
33166
+ exchange: "supported",
33167
+ activeIntegrations: integrations.filter(
33168
+ (item) => item.expiresAt > Date.now()
33169
+ ).length
33170
+ },
33171
+ development: {
33172
+ status: developmentMissing.length || authIssues.length ? "blocked" : "configured",
33173
+ missing: developmentMissing,
33174
+ authentication: authIssues
33175
+ },
33176
+ staging: checks.every((check2) => check2.status === "ok") ? "configured" : "blocked",
33177
+ checks
33178
+ });
33179
+ }
32355
33180
  if (path === "/repository" && ["GET", "POST"].includes(request.method)) {
32356
33181
  requirePermission(request.method === "POST" ? "publish" : "read");
32357
33182
  const publicBase = env.GIT_PUBLIC_URL;
@@ -32367,15 +33192,45 @@ async function managementRoutes(request, env) {
32367
33192
  if (repo.status !== "ready") {
32368
33193
  if (!env.HOSTING_ASSETS)
32369
33194
  throw new AccessError("Starter storage is not configured", 503);
33195
+ const active = await env.HOSTING_ASSETS.get(
33196
+ "starters/campaign-current.json"
33197
+ );
33198
+ const pointer = active ? external_exports.object({
33199
+ format: external_exports.literal(1),
33200
+ key: external_exports.string().regex(/^starters\/campaign\/[a-f0-9]{64}\.json$/),
33201
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/)
33202
+ }).parse(await active.json()) : null;
32370
33203
  const starter = await env.HOSTING_ASSETS.get(
32371
- "starters/campaign.json"
33204
+ pointer?.key ?? "starters/campaign.json"
32372
33205
  );
32373
33206
  if (!starter)
32374
33207
  throw new AccessError(
32375
33208
  "Campaign starter has not been uploaded",
32376
33209
  503
32377
33210
  );
32378
- const files = starterFilesSchema.parse(await starter.json());
33211
+ const contents = await starter.text();
33212
+ if (pointer && await hash2(contents) !== pointer.sha256)
33213
+ throw new AccessError(
33214
+ "Starter integrity check failed. Contact a Domino operator.",
33215
+ 503
33216
+ );
33217
+ const files = starterFilesSchema.parse(JSON.parse(contents));
33218
+ const setup = (await store.projects(organization)).find(
33219
+ (item) => item.project === project
33220
+ )?.setup;
33221
+ if (setup) {
33222
+ const brief = {
33223
+ path: "CAMPAIGN.md",
33224
+ encoding: "utf-8",
33225
+ content: `# Project brief
33226
+
33227
+ ${setup.brief}
33228
+ `
33229
+ };
33230
+ const index = files.findIndex((file2) => file2.path === brief.path);
33231
+ if (index < 0) files.push(brief);
33232
+ else files[index] = brief;
33233
+ }
32379
33234
  const github = new GitHubRepositories(env);
32380
33235
  const provider = await github.provision(repo.id);
32381
33236
  await github.seed(provider, files);
@@ -32403,12 +33258,26 @@ async function managementRoutes(request, env) {
32403
33258
  "Staging builds require the test environment",
32404
33259
  422
32405
33260
  );
32406
- if (!env.BUILDER || !env.HOSTING_ASSETS || !env.STAGING_DOMAIN)
32407
- throw new AccessError("Cloud builds are not configured", 503);
33261
+ if (stagingConfigurationIssues(env).length || !env.BUILDER || !env.HOSTING_ASSETS)
33262
+ throw new AccessError(
33263
+ "Cloud builds are not configured. Run domino doctor --remote; a Domino operator must enable cloud hosting.",
33264
+ 503
33265
+ );
32408
33266
  const repository = await store.repository(organization, project);
32409
33267
  if (!repository || repository.status !== "ready")
32410
33268
  throw new AccessError("Create a hosted repository first", 409);
32411
33269
  if (path === "/builds" && request.method === "POST") {
33270
+ if (productionParticipantIssues(
33271
+ hostedParticipantEnv(
33272
+ env,
33273
+ { organization, project },
33274
+ `https://stage-${repository.id.replaceAll("-", "")}.${env.STAGING_DOMAIN}`
33275
+ )
33276
+ ).length)
33277
+ throw new AccessError(
33278
+ "Hosted test sign-in is not configured. Run domino doctor --remote before staging.",
33279
+ 503
33280
+ );
32412
33281
  const input = stageRequestSchema.parse(await request.json());
32413
33282
  await new GitHubRepositories(env).verifyCommit(
32414
33283
  repository.provider,
@@ -32416,6 +33285,7 @@ async function managementRoutes(request, env) {
32416
33285
  );
32417
33286
  const build2 = await store.reserveBuild({
32418
33287
  id: input.requestId,
33288
+ runtime: "hosted-test",
32419
33289
  repository: repository.id,
32420
33290
  organization,
32421
33291
  project,
@@ -32877,7 +33747,7 @@ async function deviceRoutes(request, env) {
32877
33747
 
32878
33748
  // apps/api/src/management-store.ts
32879
33749
  init_zod();
32880
- import { DurableObject as DurableObject3 } from "cloudflare:workers";
33750
+ import { DurableObject as DurableObject4 } from "cloudflare:workers";
32881
33751
  var repositoryBase = external_exports.object({
32882
33752
  id: external_exports.string().uuid(),
32883
33753
  organization: external_exports.string(),
@@ -32893,11 +33763,12 @@ var repositoryRecord = external_exports.discriminatedUnion("status", [
32893
33763
  provider: githubRepositorySchema
32894
33764
  })
32895
33765
  ]);
32896
- var ManagementStore = class extends DurableObject3 {
33766
+ var ManagementStore = class extends DurableObject4 {
32897
33767
  constructor(ctx, env) {
32898
33768
  super(ctx, env);
32899
33769
  ctx.storage.sql.exec(`
32900
33770
  CREATE TABLE IF NOT EXISTS projects (organization TEXT, project TEXT, name TEXT NOT NULL, PRIMARY KEY(organization,project));
33771
+ CREATE TABLE IF NOT EXISTS project_setup (organization TEXT, project TEXT, data TEXT NOT NULL, PRIMARY KEY(organization,project));
32901
33772
  CREATE TABLE IF NOT EXISTS grants (organization TEXT, project TEXT, subject TEXT, data TEXT NOT NULL, PRIMARY KEY(organization,project,subject));
32902
33773
  DROP TABLE IF EXISTS tokens;
32903
33774
  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 +34031,14 @@ var ManagementStore = class extends DurableObject3 {
33160
34031
  return { total, audit };
33161
34032
  }
33162
34033
  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();
34034
+ 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";
34035
+ return this.ctx.storage.sql.exec(
34036
+ query + (organization ? " WHERE p.organization=?" : "") + " ORDER BY p.name,p.project",
34037
+ ...organization ? [organization] : []
34038
+ ).toArray().map(({ setup, ...project }) => ({
34039
+ ...project,
34040
+ ...setup ? { setup: projectSetupSchema.parse(JSON.parse(setup)) } : {}
34041
+ }));
33169
34042
  }
33170
34043
  create(organization, input, actor) {
33171
34044
  return this.ctx.storage.transactionSync(() => {
@@ -33173,7 +34046,7 @@ var ManagementStore = class extends DurableObject3 {
33173
34046
  (p) => p.project === input.project
33174
34047
  );
33175
34048
  if (existing) {
33176
- if (existing.name !== input.name)
34049
+ if (existing.name !== input.name || input.setup && JSON.stringify(existing.setup) !== JSON.stringify(input.setup))
33177
34050
  throw new Error("Project ID is already in use");
33178
34051
  return existing;
33179
34052
  }
@@ -33183,6 +34056,13 @@ var ManagementStore = class extends DurableObject3 {
33183
34056
  input.project,
33184
34057
  input.name
33185
34058
  );
34059
+ if (input.setup)
34060
+ this.ctx.storage.sql.exec(
34061
+ "INSERT INTO project_setup VALUES(?,?,?)",
34062
+ organization,
34063
+ input.project,
34064
+ JSON.stringify(input.setup)
34065
+ );
33186
34066
  this.audit(actor, organization, "project-created", input);
33187
34067
  return { organization, ...input };
33188
34068
  });
@@ -33263,6 +34143,7 @@ var photo_quest_default = { code: 'var Kd=Object.defineProperty;var ee=(e,r)=>{f
33263
34143
 
33264
34144
  // apps/api/src/auth-routes.ts
33265
34145
  init_zod();
34146
+ init_dist();
33266
34147
 
33267
34148
  // apps/api/src/oauth-routes.ts
33268
34149
  init_zod();
@@ -33559,6 +34440,8 @@ async function oauthRoutes(request, env, session) {
33559
34440
 
33560
34441
  // apps/api/src/auth-routes.ts
33561
34442
  var emailFlowSchema = external_exports.object({
34443
+ scope: authScopeSchema,
34444
+ origin: external_exports.string(),
33562
34445
  email: external_exports.string().email(),
33563
34446
  digest: external_exports.string(),
33564
34447
  binding: external_exports.string()
@@ -33602,6 +34485,8 @@ async function authRoutes(request, env, session) {
33602
34485
  id2,
33603
34486
  {
33604
34487
  email: email3,
34488
+ scope: authScope(env),
34489
+ origin: request.headers.get("Origin") ?? "",
33605
34490
  digest: await hash2(id2 + ":" + code),
33606
34491
  binding: await hash2(binding)
33607
34492
  },
@@ -33614,6 +34499,8 @@ async function authRoutes(request, env, session) {
33614
34499
  id2,
33615
34500
  JSON.stringify({
33616
34501
  email: email3,
34502
+ scope: authScope(env),
34503
+ origin: request.headers.get("Origin") ?? "",
33617
34504
  digest: await hash2(id2 + ":" + code),
33618
34505
  binding: await hash2(binding)
33619
34506
  })
@@ -33638,7 +34525,12 @@ async function authRoutes(request, env, session) {
33638
34525
  const raw = await auth.readFlow(challenge);
33639
34526
  if (!raw || !await auth.rateLimit("verify:" + challenge, 5, 6e5))
33640
34527
  return fail("Code expired or too many attempts. Request another.");
33641
- const flow = emailFlowSchema.parse(JSON.parse(raw));
34528
+ const parsed = emailFlowSchema.safeParse(JSON.parse(raw));
34529
+ if (!parsed.success) return fail("Code expired. Request another.");
34530
+ const flow = parsed.data;
34531
+ const scope = authScope(env);
34532
+ if (flow.scope.organization !== scope.organization || flow.scope.project !== scope.project || flow.scope.environment !== scope.environment || flow.origin !== (request.headers.get("Origin") ?? ""))
34533
+ return fail("Sign-in scope changed. Request another code.");
33642
34534
  if (flow.binding !== await hash2(cookie(request, "relay_auth_flow")) || flow.digest !== await hash2(challenge + ":" + code))
33643
34535
  return fail("Invalid code");
33644
34536
  if (!await auth.consumeFlow(challenge, raw))
@@ -33660,7 +34552,7 @@ async function authRoutes(request, env, session) {
33660
34552
 
33661
34553
  // apps/api/src/index.ts
33662
34554
  var gateway = {
33663
- async fetch(request, env) {
34555
+ async fetch(request, env, identityIntegration) {
33664
34556
  const git = await gitGateway(request, env);
33665
34557
  if (git) return git;
33666
34558
  const device = await deviceRoutes(request, env);
@@ -33677,7 +34569,7 @@ var gateway = {
33677
34569
  const url2 = new URL(request.url);
33678
34570
  const local = localParticipantRequest(request, env);
33679
34571
  if (!local) {
33680
- const issues = productionParticipantIssues(env);
34572
+ const issues = identityIntegration ? [] : productionParticipantIssues(env);
33681
34573
  if (issues.length)
33682
34574
  return Response.json(
33683
34575
  { error: "Participant access is not configured for this deployment" },
@@ -33691,13 +34583,13 @@ var gateway = {
33691
34583
  { error: "Participant project has not been provisioned" },
33692
34584
  { status: 503 }
33693
34585
  );
33694
- if (url2.pathname === "/v1/auth/exchange" || url2.pathname === "/v1/auth/demo")
34586
+ if (url2.pathname === "/v1/auth/demo")
33695
34587
  return Response.json({ error: "Route not found" }, { status: 404 });
33696
34588
  }
33697
34589
  const auth = env.AUTH.getByName("auth-v1");
33698
34590
  const token = sessionToken(request);
33699
34591
  const management = local && !!env.API_TOKEN && request.headers.get("Authorization") === "Bearer " + env.API_TOKEN;
33700
- const resolvedSession = management ? null : token ? await auth.resolve(token) : null;
34592
+ const resolvedSession = management ? null : token ? await auth.resolve(token, identityIntegration) : null;
33701
34593
  const portalScope = authScope(env);
33702
34594
  const session = resolvedSession && (!local || !request.headers.has("Authorization")) && (resolvedSession.organization !== portalScope.organization || resolvedSession.project !== portalScope.project || resolvedSession.environment !== portalScope.environment) ? null : resolvedSession;
33703
34595
  if (!local && session && session.principal.kind !== "member")
@@ -34067,6 +34959,11 @@ var gateway = {
34067
34959
  }
34068
34960
  }
34069
34961
  };
34962
+ var PreviewGateway = class extends WorkerEntrypoint3 {
34963
+ async fetch(request) {
34964
+ return developmentPreview(request, this.env, gateway.fetch);
34965
+ }
34966
+ };
34070
34967
  var StagingGateway = class extends WorkerEntrypoint3 {
34071
34968
  async fetch(request) {
34072
34969
  return await stagingRequest(request, this.env, gateway.fetch) ?? new Response("Staging not found", { status: 404 });
@@ -34074,7 +34971,11 @@ var StagingGateway = class extends WorkerEntrypoint3 {
34074
34971
  };
34075
34972
  var index_default = {
34076
34973
  async fetch(request, env) {
34077
- const response = await gateway.fetch(request, env);
34974
+ const response = await identityIntegrationRequest(
34975
+ request,
34976
+ env,
34977
+ (incoming, scoped, id2) => gateway.fetch(incoming, scoped, id2)
34978
+ ) ?? await gateway.fetch(request, await callbackEnvironment(request, env));
34078
34979
  const headers = new Headers(response.headers);
34079
34980
  headers.set("Cache-Control", "no-store");
34080
34981
  return new Response(response.body, { status: response.status, headers });
@@ -34086,6 +34987,8 @@ export {
34086
34987
  HostingControl,
34087
34988
  ManagementStore,
34088
34989
  OrganizationLedger,
34990
+ PreviewGateway,
34991
+ PreviewSession,
34089
34992
  StagingAuth,
34090
34993
  StagingGateway,
34091
34994
  StagingLedger,