@wtfalch/foundry 0.46.0 → 0.48.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/build-json.d.ts +28 -0
- package/dist/build-json.js +95 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +6 -2
- package/dist/migrations/0058_app_requests.sql +32 -0
- package/dist/migrations/0059_org_builtin_blue_green.sql +9 -0
- package/dist/org.d.ts +47 -5
- package/dist/org.js +176 -128
- package/dist/plan/archon-blue-green.js +15 -1
- package/dist/plan/blue-green-apply.d.ts +35 -0
- package/dist/plan/blue-green-apply.js +167 -16
- package/dist/plan/blue-green.d.ts +97 -2
- package/dist/plan/blue-green.js +214 -13
- package/dist/plan/database-dump.d.ts +37 -0
- package/dist/plan/database-dump.js +101 -0
- package/dist/plan/diff.js +9 -5
- package/dist/plan/executor.d.ts +48 -0
- package/dist/plan/executor.js +87 -28
- package/dist/plan/health-targets.js +1 -2
- package/dist/plan/hostnames.d.ts +2 -2
- package/dist/plan/hostnames.js +6 -6
- package/dist/plan/index.d.ts +3 -1
- package/dist/plan/index.js +5 -1
- package/dist/plan/locked-hostnames.d.ts +1 -0
- package/dist/plan/locked-hostnames.js +1 -0
- package/dist/plan/plan-apply.d.ts +30 -1
- package/dist/plan/plan-apply.js +86 -8
- package/dist/plan/platform-diff.js +2 -1
- package/dist/plan/platform-providers.js +5 -0
- package/dist/plan/providers-readers.js +15 -4
- package/dist/plan/proxy-labels.d.ts +13 -1
- package/dist/plan/proxy-labels.js +26 -3
- package/dist/plan/release-host.d.ts +1 -1
- package/dist/plan/release-host.js +36 -14
- package/dist/plan/stage-env.d.ts +38 -0
- package/dist/plan/stage-env.js +52 -0
- package/dist/plan/stage-guards.d.ts +104 -0
- package/dist/plan/stage-guards.js +149 -0
- package/dist/plan/stage-isolation.d.ts +57 -0
- package/dist/plan/stage-isolation.js +248 -0
- package/dist/plan/worker-app.js +3 -1
- package/dist/refuse-org.d.ts +7 -0
- package/dist/refuse-org.js +12 -0
- package/dist/release/decide.d.ts +5 -0
- package/dist/release/decide.js +10 -6
- package/dist/release/health.d.ts +6 -1
- package/dist/release/health.js +10 -2
- package/dist/release/run.d.ts +2 -0
- package/dist/release/run.js +2 -1
- package/dist/requested-changes.d.ts +50 -4
- package/dist/requested-changes.js +9 -1
- package/dist/store/postgres.d.ts +5 -1
- package/dist/store/postgres.js +86 -8
- package/dist/store/schema.d.ts +111 -1
- package/dist/store/schema.js +7 -0
- package/dist/store.d.ts +24 -1
- package/dist/worker-app-config.d.ts +8 -2
- package/dist/worker-app-config.js +20 -6
- package/package.json +1 -1
package/dist/store/postgres.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, asc, desc, eq, inArray, isNull, ne, sql } from 'drizzle-orm';
|
|
2
|
-
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBlueGreen, validateBuyerIdentity, validateCalls,
|
|
2
|
+
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, refuseBuiltinBlueGreen, refuseCustomers, refuseHosting, validateBlueGreen, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
|
|
3
3
|
import { hostingNames } from '../plan/locked-hostnames.js';
|
|
4
4
|
import { findNameClashes, nameClashMessage } from '../plan/org-names.js';
|
|
5
5
|
import { validatePlatform } from '../platform.js';
|
|
@@ -77,9 +77,16 @@ export function uniqueViolationConstraint(error) {
|
|
|
77
77
|
* is that throw.
|
|
78
78
|
*/
|
|
79
79
|
export class DeclaredOrgError extends Error {
|
|
80
|
-
|
|
80
|
+
/** Set when a hosting or customers validator refused the write: which rule, and where in the org. */
|
|
81
|
+
code;
|
|
82
|
+
path;
|
|
83
|
+
constructor(message, refusal) {
|
|
81
84
|
super(message);
|
|
82
85
|
this.name = 'DeclaredOrgError';
|
|
86
|
+
if (refusal) {
|
|
87
|
+
this.code = refusal.code;
|
|
88
|
+
this.path = refusal.path;
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
/**
|
|
@@ -154,6 +161,12 @@ function rowToOrg(row) {
|
|
|
154
161
|
? { customers: { enabled: row.customersEnabled, signUp: row.customersSignUp } }
|
|
155
162
|
: {}),
|
|
156
163
|
// Omitted while unset. Validated on read: a stored value is not trusted.
|
|
164
|
+
...(row.bouleBlueGreen
|
|
165
|
+
? { boule: { blueGreen: readBlueGreen(row.bouleBlueGreen, 'boule') } }
|
|
166
|
+
: {}),
|
|
167
|
+
...(row.agoraBlueGreen
|
|
168
|
+
? { agora: { blueGreen: readBlueGreen(row.agoraBlueGreen, 'agora') } }
|
|
169
|
+
: {}),
|
|
157
170
|
...(row.versionPin ? { versionPin: readVersionPin(row) } : {}),
|
|
158
171
|
site: { enabled: row.siteEnabled, usesDomain: row.siteUsesDomain },
|
|
159
172
|
hosting: {
|
|
@@ -186,6 +199,13 @@ function readVersionPin(row) {
|
|
|
186
199
|
}
|
|
187
200
|
return pin;
|
|
188
201
|
}
|
|
202
|
+
function readBlueGreen(stored, app) {
|
|
203
|
+
const reason = validateBlueGreen(stored, app);
|
|
204
|
+
if (reason) {
|
|
205
|
+
throw new DeclaredOrgError(`a stored org has an invalid ${app}.blueGreen: ${reason}`);
|
|
206
|
+
}
|
|
207
|
+
return stored;
|
|
208
|
+
}
|
|
189
209
|
function orgToRow(org) {
|
|
190
210
|
return {
|
|
191
211
|
id: org.id,
|
|
@@ -208,6 +228,8 @@ function orgToRow(org) {
|
|
|
208
228
|
signInInApp: org.signIn?.inApp ?? false,
|
|
209
229
|
customersEnabled: org.customers?.enabled ?? false,
|
|
210
230
|
customersSignUp: org.customers?.signUp ?? false,
|
|
231
|
+
bouleBlueGreen: org.boule?.blueGreen ?? null,
|
|
232
|
+
agoraBlueGreen: org.agora?.blueGreen ?? null,
|
|
211
233
|
servicesOwn: org.services?.own ?? false,
|
|
212
234
|
servicesSfuSlot: org.services?.sfuSlot ?? null,
|
|
213
235
|
versionPin: org.versionPin && Object.keys(org.versionPin).length > 0 ? org.versionPin : null,
|
|
@@ -357,9 +379,9 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
357
379
|
// #71: same place `requireValidBuyer` checks the buyer identity (#67), on
|
|
358
380
|
// both `create` and the merged result of `update`.
|
|
359
381
|
function requireValidHosting(org) {
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
362
|
-
throw new DeclaredOrgError(`hosting invalid: ${
|
|
382
|
+
const refusal = refuseHosting(org.hosting, hostingNames(org, []));
|
|
383
|
+
if (refusal)
|
|
384
|
+
throw new DeclaredOrgError(`hosting invalid: ${refusal.message} (#71)`, refusal);
|
|
363
385
|
}
|
|
364
386
|
// #101: same place `requireValidHosting` checks hosting, on both `create`
|
|
365
387
|
// and the merged result of `update`.
|
|
@@ -411,9 +433,14 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
411
433
|
const signInReason = validateSignIn(org);
|
|
412
434
|
if (signInReason)
|
|
413
435
|
throw new DeclaredOrgError(`signIn invalid: ${signInReason}`);
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
416
|
-
throw new DeclaredOrgError(`customers invalid: ${
|
|
436
|
+
const customersRefusal = refuseCustomers(org);
|
|
437
|
+
if (customersRefusal) {
|
|
438
|
+
throw new DeclaredOrgError(`customers invalid: ${customersRefusal.message}`, customersRefusal);
|
|
439
|
+
}
|
|
440
|
+
const builtinRefusal = refuseBuiltinBlueGreen(org);
|
|
441
|
+
if (builtinRefusal) {
|
|
442
|
+
throw new DeclaredOrgError(`blue/green invalid: ${builtinRefusal.message}`, builtinRefusal);
|
|
443
|
+
}
|
|
417
444
|
const slot = org.services?.sfuSlot ?? null;
|
|
418
445
|
if (slot === null)
|
|
419
446
|
return;
|
|
@@ -855,6 +882,10 @@ function rowToRequestedChange(row) {
|
|
|
855
882
|
requestedBy: row.requestedBy,
|
|
856
883
|
createdAt: row.createdAt.toISOString(),
|
|
857
884
|
state: row.state,
|
|
885
|
+
requester: row.requester ?? null,
|
|
886
|
+
idempotencyKey: row.idempotencyKey ?? null,
|
|
887
|
+
reason: row.reason ?? null,
|
|
888
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
858
889
|
};
|
|
859
890
|
}
|
|
860
891
|
/**
|
|
@@ -885,6 +916,53 @@ export function createPostgresRequestedChangeStore(db) {
|
|
|
885
916
|
throw new RequestedChangeError('insert returned no row');
|
|
886
917
|
return rowToRequestedChange(row);
|
|
887
918
|
},
|
|
919
|
+
async createOrGet({ orgId, value, requestedBy, requester, idempotencyKey }) {
|
|
920
|
+
if (value.field !== 'app' && value.field !== 'build') {
|
|
921
|
+
throw new RequestedChangeError(`'${value.field}' is not an app or build request`);
|
|
922
|
+
}
|
|
923
|
+
if (idempotencyKey === '')
|
|
924
|
+
throw new RequestedChangeError('an idempotency key is required');
|
|
925
|
+
const [inserted] = await db
|
|
926
|
+
.insert(foundryRequestedChanges)
|
|
927
|
+
.values({
|
|
928
|
+
orgId,
|
|
929
|
+
field: value.field,
|
|
930
|
+
value: requestedChangePayload(value),
|
|
931
|
+
requestedBy,
|
|
932
|
+
state: 'applying',
|
|
933
|
+
requester: { ...requester },
|
|
934
|
+
idempotencyKey,
|
|
935
|
+
})
|
|
936
|
+
.onConflictDoNothing()
|
|
937
|
+
.returning();
|
|
938
|
+
if (inserted)
|
|
939
|
+
return { request: rowToRequestedChange(inserted), created: true };
|
|
940
|
+
const [existing] = await db
|
|
941
|
+
.select()
|
|
942
|
+
.from(foundryRequestedChanges)
|
|
943
|
+
.where(and(eq(foundryRequestedChanges.orgId, orgId), eq(foundryRequestedChanges.idempotencyKey, idempotencyKey)))
|
|
944
|
+
.limit(1);
|
|
945
|
+
if (!existing)
|
|
946
|
+
throw new RequestedChangeError('insert returned no row');
|
|
947
|
+
return { request: rowToRequestedChange(existing), created: false };
|
|
948
|
+
},
|
|
949
|
+
async transition(id, from, to, reason) {
|
|
950
|
+
const [row] = await db
|
|
951
|
+
.update(foundryRequestedChanges)
|
|
952
|
+
.set({ state: to, reason: reason ? { ...reason } : null, updatedAt: new Date() })
|
|
953
|
+
.where(and(eq(foundryRequestedChanges.id, id), inArray(foundryRequestedChanges.state, [...from])))
|
|
954
|
+
.returning();
|
|
955
|
+
return row ? rowToRequestedChange(row) : null;
|
|
956
|
+
},
|
|
957
|
+
async openForApp(orgId, key) {
|
|
958
|
+
const [row] = await db
|
|
959
|
+
.select()
|
|
960
|
+
.from(foundryRequestedChanges)
|
|
961
|
+
.where(and(eq(foundryRequestedChanges.orgId, orgId), eq(foundryRequestedChanges.state, 'applying'), inArray(foundryRequestedChanges.field, ['app', 'build']), sql `${foundryRequestedChanges.value} ->> 'key' = ${key}`))
|
|
962
|
+
.orderBy(desc(foundryRequestedChanges.id))
|
|
963
|
+
.limit(1);
|
|
964
|
+
return row ? rowToRequestedChange(row) : null;
|
|
965
|
+
},
|
|
888
966
|
async get(id) {
|
|
889
967
|
const [row] = await db
|
|
890
968
|
.select()
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppEnvSource, HostingEntry, IssuerApp, OwnerIdentity } from '../org.js';
|
|
1
|
+
import type { AppEnvSource, BlueGreenConfig, HostingEntry, IssuerApp, OwnerIdentity } from '../org.js';
|
|
2
2
|
import type { Plan } from '../plan.js';
|
|
3
3
|
import type { Checkpoint, DestroyRecord } from '../state.js';
|
|
4
4
|
/**
|
|
@@ -406,6 +406,44 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
406
406
|
identity: undefined;
|
|
407
407
|
generated: undefined;
|
|
408
408
|
}, {}, {}>;
|
|
409
|
+
bouleBlueGreen: import("drizzle-orm/pg-core").PgColumn<{
|
|
410
|
+
name: "boule_blue_green";
|
|
411
|
+
tableName: "foundry_declared_orgs";
|
|
412
|
+
dataType: "json";
|
|
413
|
+
columnType: "PgJsonb";
|
|
414
|
+
data: BlueGreenConfig | null;
|
|
415
|
+
driverParam: unknown;
|
|
416
|
+
notNull: false;
|
|
417
|
+
hasDefault: false;
|
|
418
|
+
isPrimaryKey: false;
|
|
419
|
+
isAutoincrement: false;
|
|
420
|
+
hasRuntimeDefault: false;
|
|
421
|
+
enumValues: undefined;
|
|
422
|
+
baseColumn: never;
|
|
423
|
+
identity: undefined;
|
|
424
|
+
generated: undefined;
|
|
425
|
+
}, {}, {
|
|
426
|
+
$type: BlueGreenConfig | null;
|
|
427
|
+
}>;
|
|
428
|
+
agoraBlueGreen: import("drizzle-orm/pg-core").PgColumn<{
|
|
429
|
+
name: "agora_blue_green";
|
|
430
|
+
tableName: "foundry_declared_orgs";
|
|
431
|
+
dataType: "json";
|
|
432
|
+
columnType: "PgJsonb";
|
|
433
|
+
data: BlueGreenConfig | null;
|
|
434
|
+
driverParam: unknown;
|
|
435
|
+
notNull: false;
|
|
436
|
+
hasDefault: false;
|
|
437
|
+
isPrimaryKey: false;
|
|
438
|
+
isAutoincrement: false;
|
|
439
|
+
hasRuntimeDefault: false;
|
|
440
|
+
enumValues: undefined;
|
|
441
|
+
baseColumn: never;
|
|
442
|
+
identity: undefined;
|
|
443
|
+
generated: undefined;
|
|
444
|
+
}, {}, {
|
|
445
|
+
$type: BlueGreenConfig | null;
|
|
446
|
+
}>;
|
|
409
447
|
versionPin: import("drizzle-orm/pg-core").PgColumn<{
|
|
410
448
|
name: "version_pin";
|
|
411
449
|
tableName: "foundry_declared_orgs";
|
|
@@ -2470,6 +2508,78 @@ export declare const foundryRequestedChanges: import("drizzle-orm/pg-core").PgTa
|
|
|
2470
2508
|
identity: undefined;
|
|
2471
2509
|
generated: undefined;
|
|
2472
2510
|
}, {}, {}>;
|
|
2511
|
+
requester: import("drizzle-orm/pg-core").PgColumn<{
|
|
2512
|
+
name: "requester";
|
|
2513
|
+
tableName: "foundry_requested_changes";
|
|
2514
|
+
dataType: "json";
|
|
2515
|
+
columnType: "PgJsonb";
|
|
2516
|
+
data: Record<string, unknown>;
|
|
2517
|
+
driverParam: unknown;
|
|
2518
|
+
notNull: false;
|
|
2519
|
+
hasDefault: false;
|
|
2520
|
+
isPrimaryKey: false;
|
|
2521
|
+
isAutoincrement: false;
|
|
2522
|
+
hasRuntimeDefault: false;
|
|
2523
|
+
enumValues: undefined;
|
|
2524
|
+
baseColumn: never;
|
|
2525
|
+
identity: undefined;
|
|
2526
|
+
generated: undefined;
|
|
2527
|
+
}, {}, {
|
|
2528
|
+
$type: Record<string, unknown>;
|
|
2529
|
+
}>;
|
|
2530
|
+
idempotencyKey: import("drizzle-orm/pg-core").PgColumn<{
|
|
2531
|
+
name: "idempotency_key";
|
|
2532
|
+
tableName: "foundry_requested_changes";
|
|
2533
|
+
dataType: "string";
|
|
2534
|
+
columnType: "PgText";
|
|
2535
|
+
data: string;
|
|
2536
|
+
driverParam: string;
|
|
2537
|
+
notNull: false;
|
|
2538
|
+
hasDefault: false;
|
|
2539
|
+
isPrimaryKey: false;
|
|
2540
|
+
isAutoincrement: false;
|
|
2541
|
+
hasRuntimeDefault: false;
|
|
2542
|
+
enumValues: [string, ...string[]];
|
|
2543
|
+
baseColumn: never;
|
|
2544
|
+
identity: undefined;
|
|
2545
|
+
generated: undefined;
|
|
2546
|
+
}, {}, {}>;
|
|
2547
|
+
reason: import("drizzle-orm/pg-core").PgColumn<{
|
|
2548
|
+
name: "reason";
|
|
2549
|
+
tableName: "foundry_requested_changes";
|
|
2550
|
+
dataType: "json";
|
|
2551
|
+
columnType: "PgJsonb";
|
|
2552
|
+
data: Record<string, unknown>;
|
|
2553
|
+
driverParam: unknown;
|
|
2554
|
+
notNull: false;
|
|
2555
|
+
hasDefault: false;
|
|
2556
|
+
isPrimaryKey: false;
|
|
2557
|
+
isAutoincrement: false;
|
|
2558
|
+
hasRuntimeDefault: false;
|
|
2559
|
+
enumValues: undefined;
|
|
2560
|
+
baseColumn: never;
|
|
2561
|
+
identity: undefined;
|
|
2562
|
+
generated: undefined;
|
|
2563
|
+
}, {}, {
|
|
2564
|
+
$type: Record<string, unknown>;
|
|
2565
|
+
}>;
|
|
2566
|
+
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
2567
|
+
name: "updated_at";
|
|
2568
|
+
tableName: "foundry_requested_changes";
|
|
2569
|
+
dataType: "date";
|
|
2570
|
+
columnType: "PgTimestamp";
|
|
2571
|
+
data: Date;
|
|
2572
|
+
driverParam: string;
|
|
2573
|
+
notNull: true;
|
|
2574
|
+
hasDefault: true;
|
|
2575
|
+
isPrimaryKey: false;
|
|
2576
|
+
isAutoincrement: false;
|
|
2577
|
+
hasRuntimeDefault: false;
|
|
2578
|
+
enumValues: undefined;
|
|
2579
|
+
baseColumn: never;
|
|
2580
|
+
identity: undefined;
|
|
2581
|
+
generated: undefined;
|
|
2582
|
+
}, {}, {}>;
|
|
2473
2583
|
};
|
|
2474
2584
|
dialect: "pg";
|
|
2475
2585
|
}>;
|
package/dist/store/schema.js
CHANGED
|
@@ -43,6 +43,9 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
43
43
|
// CustomersConfig (org.ts); migrations/0057.
|
|
44
44
|
customersEnabled: boolean('customers_enabled').notNull().default(false),
|
|
45
45
|
customersSignUp: boolean('customers_sign_up').notNull().default(false),
|
|
46
|
+
// OrgBuiltinApp.blueGreen (org.ts) of Boule and Agora; `migrations/0059_org_builtin_blue_green.sql`. Null is off.
|
|
47
|
+
bouleBlueGreen: jsonb('boule_blue_green').$type(),
|
|
48
|
+
agoraBlueGreen: jsonb('agora_blue_green').$type(),
|
|
46
49
|
// VersionPin (org.ts): null follows the platform. `migrations/0053_org_version_pin.sql`.
|
|
47
50
|
versionPin: jsonb('version_pin').$type(),
|
|
48
51
|
// SiteConfig (org.ts, T10). A brand new org has no cms site.
|
|
@@ -234,6 +237,10 @@ export const foundryRequestedChanges = pgTable('foundry_requested_changes', {
|
|
|
234
237
|
requestedBy: text('requested_by').notNull(),
|
|
235
238
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
236
239
|
state: text('state').notNull().default('needs_approval'),
|
|
240
|
+
requester: jsonb('requester').$type(),
|
|
241
|
+
idempotencyKey: text('idempotency_key'),
|
|
242
|
+
reason: jsonb('reason').$type(),
|
|
243
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
237
244
|
});
|
|
238
245
|
/** Mirrors `migrations/0055_release_requests.sql`: an operator's release action, consumed once by the runner. */
|
|
239
246
|
export const foundryReleaseRequests = pgTable('foundry_release_requests', {
|
package/dist/store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DeclaredOrg, OrgId } from './org.js';
|
|
2
2
|
import type { DeclaredPlatform } from './platform.js';
|
|
3
|
-
import type { RequestedChange, RequestedChangeValue } from './requested-changes.js';
|
|
3
|
+
import type { RequestedBy, RequestedChange, RequestedChangeState, RequestedChangeValue } from './requested-changes.js';
|
|
4
4
|
import type { AppliedState } from './state.js';
|
|
5
5
|
/** `DeclaredOrgStore.update`'s options (#351). */
|
|
6
6
|
export interface UpdateOptions {
|
|
@@ -190,6 +190,29 @@ export interface RequestedChangeStore {
|
|
|
190
190
|
value: RequestedChangeValue;
|
|
191
191
|
requestedBy: string;
|
|
192
192
|
}): Promise<RequestedChange>;
|
|
193
|
+
/**
|
|
194
|
+
* Stores an `app` or `build` request in `applying`, or returns the one already stored under the
|
|
195
|
+
* same (orgId, idempotencyKey) with `created: false` and writes nothing. Safe under a race.
|
|
196
|
+
* Throws `RequestedChangeError` for any other kind.
|
|
197
|
+
*/
|
|
198
|
+
createOrGet(input: {
|
|
199
|
+
orgId: OrgId;
|
|
200
|
+
org: DeclaredOrg;
|
|
201
|
+
value: RequestedChangeValue;
|
|
202
|
+
requestedBy: string;
|
|
203
|
+
requester: RequestedBy;
|
|
204
|
+
idempotencyKey: string;
|
|
205
|
+
}): Promise<{
|
|
206
|
+
request: RequestedChange;
|
|
207
|
+
created: boolean;
|
|
208
|
+
}>;
|
|
209
|
+
/**
|
|
210
|
+
* Compare-and-set: moves request `id` to `to` only when it is in one of `from`, and returns the
|
|
211
|
+
* new row; null when it was not (or does not exist). `reason` is stored (null when omitted).
|
|
212
|
+
*/
|
|
213
|
+
transition(id: number, from: readonly RequestedChangeState[], to: RequestedChangeState, reason?: RequestedChange['reason']): Promise<RequestedChange | null>;
|
|
214
|
+
/** The `applying` app or build request for entry `key` of that org, or null. */
|
|
215
|
+
openForApp(orgId: OrgId, key: string): Promise<RequestedChange | null>;
|
|
193
216
|
get(id: number): Promise<RequestedChange | null>;
|
|
194
217
|
/** Every request an org has made, oldest first. */
|
|
195
218
|
listForOrg(orgId: OrgId): Promise<RequestedChange[]>;
|
|
@@ -117,13 +117,19 @@ export declare function validateArtifact(at: string, artifactUrl: string, sha256
|
|
|
117
117
|
* The one exact name a Worker app is served at: `<host>.<domain>` or the apex
|
|
118
118
|
* on the org's own domain, else `<slug>-<key>.oikos.host` (one label under
|
|
119
119
|
* `oikos.host`, so the free Universal certificate covers it), and
|
|
120
|
-
* `<slug>.oikos.host` for the entry
|
|
120
|
+
* `<slug>.oikos.host` for the main entry.
|
|
121
121
|
*/
|
|
122
122
|
export declare function workerAppHostnameOf(org: {
|
|
123
123
|
slug: string;
|
|
124
124
|
domain: string | null;
|
|
125
125
|
usesDomain: boolean;
|
|
126
|
-
},
|
|
126
|
+
}, entry: Pick<HostingEntry, 'key' | 'main'>, host: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* A Worker app declared before its first build has an empty `artifactUrl` and an empty
|
|
129
|
+
* `artifactSha256`. It validates, and the planner deploys nothing until both are set. One empty
|
|
130
|
+
* and one set is still refused.
|
|
131
|
+
*/
|
|
132
|
+
export declare function workerAppHasBuild(app: Pick<WorkerAppConfig, 'artifactUrl' | 'artifactSha256'>): boolean;
|
|
127
133
|
/**
|
|
128
134
|
* The Durable Object classes the migrations leave behind, folded in order:
|
|
129
135
|
* created, then removed, then renamed. A rename of a class that does not exist
|
|
@@ -106,12 +106,24 @@ export function validateArtifact(at, artifactUrl, sha256, repo = null) {
|
|
|
106
106
|
* The one exact name a Worker app is served at: `<host>.<domain>` or the apex
|
|
107
107
|
* on the org's own domain, else `<slug>-<key>.oikos.host` (one label under
|
|
108
108
|
* `oikos.host`, so the free Universal certificate covers it), and
|
|
109
|
-
* `<slug>.oikos.host` for the entry
|
|
109
|
+
* `<slug>.oikos.host` for the main entry.
|
|
110
110
|
*/
|
|
111
|
-
export function workerAppHostnameOf(org,
|
|
111
|
+
export function workerAppHostnameOf(org, entry, host) {
|
|
112
112
|
if (org.usesDomain && org.domain)
|
|
113
113
|
return host === '@' ? org.domain : `${host}.${org.domain}`;
|
|
114
|
-
return
|
|
114
|
+
return isMain(entry) ? `${org.slug}.oikos.host` : `${org.slug}-${entry.key}.oikos.host`;
|
|
115
|
+
}
|
|
116
|
+
/** `org.ts`'s `isMainEntry`, restated: that file imports this one. */
|
|
117
|
+
function isMain(entry) {
|
|
118
|
+
return entry.main ?? entry.key === 'app';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A Worker app declared before its first build has an empty `artifactUrl` and an empty
|
|
122
|
+
* `artifactSha256`. It validates, and the planner deploys nothing until both are set. One empty
|
|
123
|
+
* and one set is still refused.
|
|
124
|
+
*/
|
|
125
|
+
export function workerAppHasBuild(app) {
|
|
126
|
+
return app.artifactUrl !== '' || app.artifactSha256 !== '';
|
|
115
127
|
}
|
|
116
128
|
/** True for a real calendar date written `YYYY-MM-DD`. */
|
|
117
129
|
function isCalendarDate(value) {
|
|
@@ -197,7 +209,9 @@ export function validateEntryWorkerApp(entry, hosting, names) {
|
|
|
197
209
|
if (entry.key.length > WORKER_NAME_MAX_LENGTH - WORKER_NAME_FIXED_LENGTH) {
|
|
198
210
|
return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "${WORKER_APP_NAME_SUFFIX}") must fit Cloudflare's ${WORKER_NAME_MAX_LENGTH}-character limit`;
|
|
199
211
|
}
|
|
200
|
-
const invalidArtifact =
|
|
212
|
+
const invalidArtifact = workerAppHasBuild(app)
|
|
213
|
+
? validateArtifact(at, app.artifactUrl, app.artifactSha256, entry.repo)
|
|
214
|
+
: null;
|
|
201
215
|
if (invalidArtifact)
|
|
202
216
|
return invalidArtifact;
|
|
203
217
|
if (app.compatibilityDate !== undefined && !isCalendarDate(app.compatibilityDate)) {
|
|
@@ -224,14 +238,14 @@ export function validateEntryWorkerApp(entry, hosting, names) {
|
|
|
224
238
|
slug: names.slug,
|
|
225
239
|
domain: names.domain,
|
|
226
240
|
usesDomain: hosting.usesDomain,
|
|
227
|
-
}, entry
|
|
241
|
+
}, entry, app.host);
|
|
228
242
|
if (names.locked.has(served))
|
|
229
243
|
return `${at} host is a name foundry owns: ${served}`;
|
|
230
244
|
// Without a domain the name is `<slug>-<key>.oikos.host`, the shape of
|
|
231
245
|
// every environment and service host of the org (`-auth`, `-agora`,
|
|
232
246
|
// `-boule`, `-stage`, `-pr-<n>`, ...). `slug.ts` already refuses a slug
|
|
233
247
|
// that ends in one of those suffixes, so the same check refuses the key.
|
|
234
|
-
if (!domain && entry
|
|
248
|
+
if (!domain && !isMain(entry) && validateSlug(`${names.slug}-${entry.key}`)) {
|
|
235
249
|
return `${at} entry key '${entry.key}' would serve ${served}, a name foundry reserves for the org's own services and environments`;
|
|
236
250
|
}
|
|
237
251
|
}
|