@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.
Files changed (59) hide show
  1. package/dist/build-json.d.ts +28 -0
  2. package/dist/build-json.js +95 -0
  3. package/dist/index.d.ts +10 -4
  4. package/dist/index.js +6 -2
  5. package/dist/migrations/0058_app_requests.sql +32 -0
  6. package/dist/migrations/0059_org_builtin_blue_green.sql +9 -0
  7. package/dist/org.d.ts +47 -5
  8. package/dist/org.js +176 -128
  9. package/dist/plan/archon-blue-green.js +15 -1
  10. package/dist/plan/blue-green-apply.d.ts +35 -0
  11. package/dist/plan/blue-green-apply.js +167 -16
  12. package/dist/plan/blue-green.d.ts +97 -2
  13. package/dist/plan/blue-green.js +214 -13
  14. package/dist/plan/database-dump.d.ts +37 -0
  15. package/dist/plan/database-dump.js +101 -0
  16. package/dist/plan/diff.js +9 -5
  17. package/dist/plan/executor.d.ts +48 -0
  18. package/dist/plan/executor.js +87 -28
  19. package/dist/plan/health-targets.js +1 -2
  20. package/dist/plan/hostnames.d.ts +2 -2
  21. package/dist/plan/hostnames.js +6 -6
  22. package/dist/plan/index.d.ts +3 -1
  23. package/dist/plan/index.js +5 -1
  24. package/dist/plan/locked-hostnames.d.ts +1 -0
  25. package/dist/plan/locked-hostnames.js +1 -0
  26. package/dist/plan/plan-apply.d.ts +30 -1
  27. package/dist/plan/plan-apply.js +86 -8
  28. package/dist/plan/platform-diff.js +2 -1
  29. package/dist/plan/platform-providers.js +5 -0
  30. package/dist/plan/providers-readers.js +15 -4
  31. package/dist/plan/proxy-labels.d.ts +13 -1
  32. package/dist/plan/proxy-labels.js +26 -3
  33. package/dist/plan/release-host.d.ts +1 -1
  34. package/dist/plan/release-host.js +36 -14
  35. package/dist/plan/stage-env.d.ts +38 -0
  36. package/dist/plan/stage-env.js +52 -0
  37. package/dist/plan/stage-guards.d.ts +104 -0
  38. package/dist/plan/stage-guards.js +149 -0
  39. package/dist/plan/stage-isolation.d.ts +57 -0
  40. package/dist/plan/stage-isolation.js +248 -0
  41. package/dist/plan/worker-app.js +3 -1
  42. package/dist/refuse-org.d.ts +7 -0
  43. package/dist/refuse-org.js +12 -0
  44. package/dist/release/decide.d.ts +5 -0
  45. package/dist/release/decide.js +10 -6
  46. package/dist/release/health.d.ts +6 -1
  47. package/dist/release/health.js +10 -2
  48. package/dist/release/run.d.ts +2 -0
  49. package/dist/release/run.js +2 -1
  50. package/dist/requested-changes.d.ts +50 -4
  51. package/dist/requested-changes.js +9 -1
  52. package/dist/store/postgres.d.ts +5 -1
  53. package/dist/store/postgres.js +86 -8
  54. package/dist/store/schema.d.ts +111 -1
  55. package/dist/store/schema.js +7 -0
  56. package/dist/store.d.ts +24 -1
  57. package/dist/worker-app-config.d.ts +8 -2
  58. package/dist/worker-app-config.js +20 -6
  59. package/package.json +1 -1
@@ -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, validateCustomers, validateDomain, validateDomainClaim, validateHosting, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
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
- constructor(message) {
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 reason = validateHosting(org.hosting, hostingNames(org, []));
361
- if (reason)
362
- throw new DeclaredOrgError(`hosting invalid: ${reason} (#71)`);
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 customersReason = validateCustomers(org);
415
- if (customersReason)
416
- throw new DeclaredOrgError(`customers invalid: ${customersReason}`);
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()
@@ -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
  }>;
@@ -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 keyed `app`.
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
- }, key: string, host: string): string;
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 keyed `app`.
109
+ * `<slug>.oikos.host` for the main entry.
110
110
  */
111
- export function workerAppHostnameOf(org, key, host) {
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 key === 'app' ? `${org.slug}.oikos.host` : `${org.slug}-${key}.oikos.host`;
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 = validateArtifact(at, app.artifactUrl, app.artifactSha256, entry.repo);
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.key, app.host);
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.key !== 'app' && validateSlug(`${names.slug}-${entry.key}`)) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",