@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 +670 -99
- package/package.json +1 -1
- package/wrangler.jsonc +14 -0
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",
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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",
|
|
15435
|
+
session: () => request("/v1/auth/session", sessionSchema2.nullable()),
|
|
15436
15436
|
demoSignIn: (code) => changeSession(
|
|
15437
|
-
() => request("/v1/auth/demo",
|
|
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",
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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: () =>
|
|
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({
|
|
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:
|
|
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
|
|
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
|
|
20545
|
-
var AuthStore = class extends
|
|
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 ?
|
|
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 ?
|
|
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([
|
|
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
|
-
|
|
21207
|
-
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
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
|
-
|
|
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/
|
|
21566
|
+
new Request("https://ledger/management/v1/deployments/publish", {
|
|
21222
21567
|
method: "POST",
|
|
21223
21568
|
headers,
|
|
21224
|
-
body: JSON.stringify({
|
|
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
|
|
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 (
|
|
32407
|
-
throw new AccessError(
|
|
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
|
|
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
|
|
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
|
-
|
|
33164
|
-
|
|
33165
|
-
organization
|
|
33166
|
-
|
|
33167
|
-
|
|
33168
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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
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": {
|