@wtfalch/foundry 0.4.0 → 0.5.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 (66) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +96 -0
  2. package/dist/bootstrap/bootstrap.js +285 -0
  3. package/dist/bootstrap/declare.d.ts +36 -0
  4. package/dist/bootstrap/declare.js +94 -0
  5. package/dist/bootstrap/drive.d.ts +40 -0
  6. package/dist/bootstrap/drive.js +51 -0
  7. package/dist/bootstrap/index.d.ts +3 -0
  8. package/dist/bootstrap/index.js +1 -0
  9. package/dist/calls-sfu-deploy-support.d.ts +9 -6
  10. package/dist/calls-sfu-deploy-support.js +9 -6
  11. package/dist/calls-sfu-keys.d.ts +21 -0
  12. package/dist/calls-sfu-keys.js +27 -0
  13. package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
  14. package/dist/calls-sfu-ssh-adapter.js +78 -0
  15. package/dist/calls-sfu-ssh-client.d.ts +49 -0
  16. package/dist/calls-sfu-ssh-client.js +82 -0
  17. package/dist/calls-sfu.d.ts +5 -4
  18. package/dist/calls-sfu.js +5 -4
  19. package/dist/index.d.ts +31 -6
  20. package/dist/index.js +16 -3
  21. package/dist/migrations/0016_platform.sql +61 -0
  22. package/dist/migrations/0017_requested_changes.sql +42 -0
  23. package/dist/migrations/0018_platform_apply.sql +19 -0
  24. package/dist/org.js +8 -0
  25. package/dist/plan/agora.d.ts +77 -0
  26. package/dist/plan/agora.js +127 -0
  27. package/dist/plan/cloudflared-connector.d.ts +70 -0
  28. package/dist/plan/cloudflared-connector.js +106 -0
  29. package/dist/plan/diff.d.ts +20 -3
  30. package/dist/plan/diff.js +631 -116
  31. package/dist/plan/executor.d.ts +475 -168
  32. package/dist/plan/executor.js +543 -175
  33. package/dist/plan/index.d.ts +22 -5
  34. package/dist/plan/index.js +12 -2
  35. package/dist/plan/jobs-scheduler-http.d.ts +29 -0
  36. package/dist/plan/jobs-scheduler-http.js +50 -0
  37. package/dist/plan/live.d.ts +51 -2
  38. package/dist/plan/live.js +8 -0
  39. package/dist/plan/plan-apply.d.ts +192 -3
  40. package/dist/plan/plan-apply.js +278 -9
  41. package/dist/plan/platform-diff.d.ts +312 -0
  42. package/dist/plan/platform-diff.js +867 -0
  43. package/dist/plan/platform-org-ports.d.ts +50 -0
  44. package/dist/plan/platform-org-ports.js +70 -0
  45. package/dist/plan/platform-org.d.ts +120 -0
  46. package/dist/plan/platform-org.js +115 -0
  47. package/dist/plan/platform-providers.d.ts +501 -0
  48. package/dist/plan/platform-providers.js +3029 -0
  49. package/dist/plan/platform-sfu.d.ts +93 -0
  50. package/dist/plan/platform-sfu.js +100 -0
  51. package/dist/plan/providers-readers.js +22 -1
  52. package/dist/plan/zitadel-machine-user.d.ts +34 -0
  53. package/dist/plan/zitadel-machine-user.js +95 -0
  54. package/dist/plan.d.ts +6 -0
  55. package/dist/platform.d.ts +55 -0
  56. package/dist/platform.js +46 -0
  57. package/dist/requested-changes.d.ts +108 -0
  58. package/dist/requested-changes.js +95 -0
  59. package/dist/slug.js +15 -2
  60. package/dist/store/audit.js +4 -2
  61. package/dist/store/postgres.d.ts +29 -1
  62. package/dist/store/postgres.js +172 -2
  63. package/dist/store/schema.d.ts +683 -0
  64. package/dist/store/schema.js +57 -0
  65. package/dist/store.d.ts +46 -0
  66. package/package.json +1 -1
@@ -0,0 +1,95 @@
1
+ export const REQUESTABLE_FIELDS = [
2
+ 'calls',
3
+ 'storageLimit',
4
+ 'aiLimit',
5
+ 'domain',
6
+ 'mailTier',
7
+ ];
8
+ /**
9
+ * Thrown by {@link classifyChange} (and by the requested-change store's
10
+ * `create`, which calls it) for a `change.field` outside
11
+ * {@link REQUESTABLE_FIELDS} -- same "throw with a reason" shape
12
+ * `store/postgres.ts`'s `DeclaredOrgError` and `PlatformError` use.
13
+ */
14
+ export class RequestedChangeError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = 'RequestedChangeError';
18
+ }
19
+ }
20
+ /**
21
+ * `org` patched by `change`, scoped to exactly the one field it names --
22
+ * the same `{ ...current, ...patch }` shape `store/postgres.ts`'s `update`
23
+ * uses for a `DeclaredOrg` patch, narrowed to a single field here since a
24
+ * requested change only ever touches one.
25
+ */
26
+ function applyRequestedChange(org, change) {
27
+ switch (change.field) {
28
+ case 'calls':
29
+ return { ...org, calls: { ...org.calls, enabled: change.enabled } };
30
+ case 'storageLimit':
31
+ return {
32
+ ...org,
33
+ storage: { limitBytes: change.limitBytes, limitMicros: change.limitMicros },
34
+ };
35
+ case 'aiLimit':
36
+ return { ...org, ai: { limitMicros: change.limitMicros } };
37
+ case 'domain':
38
+ return { ...org, domain: change.domain };
39
+ case 'mailTier':
40
+ return { ...org, mail: { ...org.mail, mode: change.mode } };
41
+ }
42
+ }
43
+ /**
44
+ * Whether `before` and `after` differ on the org's own billing fields --
45
+ * "take price changes from the org's billing fields" (#178 "How").
46
+ * `DeclaredOrg` carries no price field yet (billing itself is
47
+ * `@wtfalch/billing`, a separate package this one does not depend on), so
48
+ * this always returns `false` today. It exists as the one place
49
+ * `classifyChange` asks the question, so a later billing field lands here
50
+ * rather than reshaping `classifyChange`'s own contract.
51
+ */
52
+ function priceChanges(_before, _after) {
53
+ return false;
54
+ }
55
+ /**
56
+ * `auto` when `change` is small and safe (`calls`, `storageLimit`,
57
+ * `aiLimit`), the plan it causes has no destructive or migrating step, and
58
+ * it causes no price change on `org`'s own billing fields; `approval`
59
+ * otherwise -- every `domain` or `mailTier` change classifies `approval`
60
+ * unconditionally, since #178 "Decide" calls both migrating steps that
61
+ * always need approval, whatever direction the change moves.
62
+ *
63
+ * `plan` is the {@link PlanDetail} `change` causes -- the caller's own diff
64
+ * against live reality, from the same `computePlanDetail` (`plan/diff.ts`)
65
+ * every other apply goes through. Any `plannedSteps` entry marked
66
+ * `destroys` or `migrates` classifies `approval`, whatever `change.field`
67
+ * says: a field-name rule only ever knows what a change usually does, not
68
+ * what it does to this org's actual live state today (#184 "Why"). `plan`
69
+ * is optional because not every caller has live state to diff against yet
70
+ * (`store/postgres.ts`'s `create` classifies from `org` alone); omitting it
71
+ * falls back to the field-name rule below, unchanged from #178.
72
+ *
73
+ * Throws {@link RequestedChangeError} for a `change.field` outside
74
+ * {@link REQUESTABLE_FIELDS} -- "everything else is refused" (#178 "How").
75
+ */
76
+ export function classifyChange(org, change, plan) {
77
+ if (!REQUESTABLE_FIELDS.includes(change.field)) {
78
+ throw new RequestedChangeError(`'${change.field}' is not a requestable field`);
79
+ }
80
+ if (plan?.plannedSteps.some((planned) => planned.step.destroys || planned.step.migrates)) {
81
+ return 'approval';
82
+ }
83
+ const next = applyRequestedChange(org, change);
84
+ if (priceChanges(org, next))
85
+ return 'approval';
86
+ switch (change.field) {
87
+ case 'calls':
88
+ case 'storageLimit':
89
+ case 'aiLimit':
90
+ return 'auto';
91
+ case 'domain':
92
+ case 'mailTier':
93
+ return 'approval';
94
+ }
95
+ }
package/dist/slug.js CHANGED
@@ -118,6 +118,16 @@ const PR_SUFFIX = /-pr-\d+$/;
118
118
  const STAGE_SUFFIX = /-stage$/;
119
119
  /** `<something>-calls` — a level-1 org's own calls service hostname (foundry#145's "Decided", 2026-09-27). */
120
120
  const CALLS_SUFFIX = /-calls$/;
121
+ /**
122
+ * `<something>-agora` — a level-1 org's own Agora hostname
123
+ * (`<slug>-agora.oikos.host`, foundry#164, the 2026-09-24 layout,
124
+ * foundry#14). Every org's Agora is always on (#164 "How": no new field on
125
+ * `DeclaredOrg` to opt out of it), so this is not an opt-in reservation the
126
+ * way `CALLS_SUFFIX` guards a feature an org can decline -- any slug ending
127
+ * `-agora` collides with *every* org's own Agora hostname the moment it is
128
+ * taken, not just one that turns a feature on.
129
+ */
130
+ const AGORA_SUFFIX = /-agora$/;
121
131
  /**
122
132
  * Validates a candidate org slug against every rule #28 decided. Returns
123
133
  * `null` when the slug is acceptable, or a {@link SlugRejection} naming
@@ -138,10 +148,13 @@ export function validateSlug(slug) {
138
148
  message: `slug '${slug}' is reserved by the estate (#28) and cannot be taken by an org`,
139
149
  };
140
150
  }
141
- if (STAGE_SUFFIX.test(slug) || PR_SUFFIX.test(slug) || CALLS_SUFFIX.test(slug)) {
151
+ if (STAGE_SUFFIX.test(slug) ||
152
+ PR_SUFFIX.test(slug) ||
153
+ CALLS_SUFFIX.test(slug) ||
154
+ AGORA_SUFFIX.test(slug)) {
142
155
  return {
143
156
  reason: 'environment-suffix',
144
- message: `slug '${slug}' ends with an environment suffix ('-stage', '-pr-<digits>' or '-calls') reserved for another org's own hostname (#28, #145) — 'acme-calls' is only legal as acme's calls hostname, never as an org slug in its own right`,
157
+ message: `slug '${slug}' ends with an environment suffix ('-stage', '-pr-<digits>', '-calls' or '-agora') reserved for another org's own hostname (#28, #145, #164) — 'acme-calls' is only legal as acme's calls hostname, 'acme-agora' only as acme's Agora hostname, never as an org slug in its own right`,
145
158
  };
146
159
  }
147
160
  return null;
@@ -6,7 +6,7 @@ import { foundryAuditEvents } from './schema.js';
6
6
  */
7
7
  export function createPostgresAuditLog(db) {
8
8
  return {
9
- async recordPlan({ actor, orgId, plan, at }) {
9
+ async recordPlan({ actor, orgId, plan, target, at }) {
10
10
  await db.insert(foundryAuditEvents).values({
11
11
  at,
12
12
  kind: 'plan',
@@ -14,9 +14,10 @@ export function createPostgresAuditLog(db) {
14
14
  actorDisplay: actor.display,
15
15
  orgId,
16
16
  plan,
17
+ target: target ?? null,
17
18
  });
18
19
  },
19
- async recordApply({ actor, orgId, plan, outcome, reason, at }) {
20
+ async recordApply({ actor, orgId, plan, outcome, reason, target, at }) {
20
21
  await db.insert(foundryAuditEvents).values({
21
22
  at,
22
23
  kind: 'apply',
@@ -26,6 +27,7 @@ export function createPostgresAuditLog(db) {
26
27
  outcome,
27
28
  reason: reason ?? null,
28
29
  plan,
30
+ target: target ?? null,
29
31
  });
30
32
  },
31
33
  async recordDestroy({ actor, orgId, outcome, reason, at }) {
@@ -1,5 +1,5 @@
1
1
  import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
2
- import type { DeclaredOrgStore } from '../store.js';
2
+ import type { DeclaredOrgStore, PlatformStore, RequestedChangeStore } from '../store.js';
3
3
  /**
4
4
  * The host's own drizzle handle or a transaction open on it, same shape
5
5
  * `@wtfalch/keys`'s `HeldKeysDb` uses: postgres-js in a real host, PGlite in
@@ -48,3 +48,31 @@ export declare class DeclaredOrgError extends Error {
48
48
  * decide; `update` still merges and stores any other patch as given.
49
49
  */
50
50
  export declare function createPostgresDeclaredOrgStore(db: FoundryStoreDb): DeclaredOrgStore;
51
+ /**
52
+ * Thrown by `put` when `validatePlatform` (`../platform.js`) refuses the
53
+ * platform given to it -- same "throw with a reason" shape
54
+ * {@link DeclaredOrgError} gives `create`/`update`.
55
+ */
56
+ export declare class PlatformError extends Error {
57
+ constructor(message: string);
58
+ }
59
+ /**
60
+ * `PlatformStore` (`../store.ts`) on Postgres, for #166: get and put the one
61
+ * {@link DeclaredPlatform}, beside `createPostgresDeclaredOrgStore` above.
62
+ * `put` validates with `../platform.js`'s `validatePlatform` and then
63
+ * upserts onto the fixed singleton row `migrations/0016_platform.sql`
64
+ * declares -- the first call creates it, every later call replaces it, and
65
+ * the table's own `check (id)` constraint is what actually refuses a second
66
+ * row, not this function.
67
+ */
68
+ export declare function createPostgresPlatformStore(db: FoundryStoreDb): PlatformStore;
69
+ /**
70
+ * `RequestedChangeStore` (`../store.ts`) on Postgres, for #178: classify and
71
+ * store a company's own requested setting change on its declared org, beside
72
+ * `createPostgresDeclaredOrgStore` and `createPostgresPlatformStore` above.
73
+ * `create` classifies with `../requested-changes.js`'s `classifyChange`
74
+ * against the `org` given to it -- this store never loads the org itself,
75
+ * the same "caller already has it" choice `PlatformStore.put`'s `platform`
76
+ * argument makes -- and stores the result as the request's initial `state`.
77
+ */
78
+ export declare function createPostgresRequestedChangeStore(db: FoundryStoreDb): RequestedChangeStore;
@@ -1,7 +1,9 @@
1
- import { eq, sql } from 'drizzle-orm';
1
+ import { asc, eq, sql } from 'drizzle-orm';
2
2
  import { validateBuyerIdentity, validateDomain, validateHosting, validateOwner, validateSite, } from '../org.js';
3
+ import { validatePlatform } from '../platform.js';
4
+ import { RequestedChangeError, classifyChange } from '../requested-changes.js';
3
5
  import { validateSlug } from '../slug.js';
4
- import { declaredOrgs, foundryAppliedState, } from './schema.js';
6
+ import { declaredOrgs, foundryAppliedState, foundryDeclaredPlatform, foundryRequestedChanges, } from './schema.js';
5
7
  const UNIQUE_VIOLATION = '23505';
6
8
  /** `migrations/0004_slug_unique.sql` / `migrations/0015_domain_unique.sql`'s index names, read back off a caught unique-violation to tell which one fired. */
7
9
  const SLUG_UNIQUE_INDEX = 'foundry_declared_orgs_slug_idx';
@@ -405,3 +407,171 @@ export function createPostgresDeclaredOrgStore(db) {
405
407
  },
406
408
  };
407
409
  }
410
+ /**
411
+ * Thrown by `put` when `validatePlatform` (`../platform.js`) refuses the
412
+ * platform given to it -- same "throw with a reason" shape
413
+ * {@link DeclaredOrgError} gives `create`/`update`.
414
+ */
415
+ export class PlatformError extends Error {
416
+ constructor(message) {
417
+ super(message);
418
+ this.name = 'PlatformError';
419
+ }
420
+ }
421
+ /** `migrations/0016_platform.sql`'s singleton row -- `id` is always `true`, the only value its `check (id)` constraint allows. */
422
+ const PLATFORM_ROW_ID = true;
423
+ function rowToPlatform(row) {
424
+ return {
425
+ ownerOrgId: row.ownerOrgId,
426
+ apex: row.apex,
427
+ auth: { enabled: row.authEnabled, hostname: row.authHostname, gitRef: row.authGitRef },
428
+ mail: { enabled: row.mailEnabled, hostname: row.mailHostname, gitRef: row.mailGitRef },
429
+ sfu: { enabled: row.sfuEnabled, hostname: row.sfuHostname, gitRef: row.sfuGitRef },
430
+ files: { enabled: row.filesEnabled, hostname: row.filesHostname, gitRef: row.filesGitRef },
431
+ ai: { enabled: row.aiEnabled, hostname: row.aiHostname, gitRef: row.aiGitRef },
432
+ jobs: { enabled: row.jobsEnabled, hostname: row.jobsHostname, gitRef: row.jobsGitRef },
433
+ integrations: {
434
+ enabled: row.integrationsEnabled,
435
+ hostname: row.integrationsHostname,
436
+ gitRef: row.integrationsGitRef,
437
+ },
438
+ archon: {
439
+ enabled: row.archonEnabled,
440
+ hostname: row.archonHostname,
441
+ gitRef: row.archonGitRef,
442
+ },
443
+ };
444
+ }
445
+ function platformToRow(platform) {
446
+ return {
447
+ id: PLATFORM_ROW_ID,
448
+ ownerOrgId: platform.ownerOrgId,
449
+ apex: platform.apex,
450
+ authEnabled: platform.auth.enabled,
451
+ authHostname: platform.auth.hostname,
452
+ authGitRef: platform.auth.gitRef,
453
+ mailEnabled: platform.mail.enabled,
454
+ mailHostname: platform.mail.hostname,
455
+ mailGitRef: platform.mail.gitRef,
456
+ sfuEnabled: platform.sfu.enabled,
457
+ sfuHostname: platform.sfu.hostname,
458
+ sfuGitRef: platform.sfu.gitRef,
459
+ filesEnabled: platform.files.enabled,
460
+ filesHostname: platform.files.hostname,
461
+ filesGitRef: platform.files.gitRef,
462
+ aiEnabled: platform.ai.enabled,
463
+ aiHostname: platform.ai.hostname,
464
+ aiGitRef: platform.ai.gitRef,
465
+ jobsEnabled: platform.jobs.enabled,
466
+ jobsHostname: platform.jobs.hostname,
467
+ jobsGitRef: platform.jobs.gitRef,
468
+ integrationsEnabled: platform.integrations.enabled,
469
+ integrationsHostname: platform.integrations.hostname,
470
+ integrationsGitRef: platform.integrations.gitRef,
471
+ archonEnabled: platform.archon.enabled,
472
+ archonHostname: platform.archon.hostname,
473
+ archonGitRef: platform.archon.gitRef,
474
+ };
475
+ }
476
+ /**
477
+ * `PlatformStore` (`../store.ts`) on Postgres, for #166: get and put the one
478
+ * {@link DeclaredPlatform}, beside `createPostgresDeclaredOrgStore` above.
479
+ * `put` validates with `../platform.js`'s `validatePlatform` and then
480
+ * upserts onto the fixed singleton row `migrations/0016_platform.sql`
481
+ * declares -- the first call creates it, every later call replaces it, and
482
+ * the table's own `check (id)` constraint is what actually refuses a second
483
+ * row, not this function.
484
+ */
485
+ export function createPostgresPlatformStore(db) {
486
+ return {
487
+ async get() {
488
+ const [row] = await db.select().from(foundryDeclaredPlatform).limit(1);
489
+ return row ? rowToPlatform(row) : null;
490
+ },
491
+ async put(platform) {
492
+ const reason = validatePlatform(platform);
493
+ if (reason)
494
+ throw new PlatformError(reason);
495
+ const row = platformToRow(platform);
496
+ await db
497
+ .insert(foundryDeclaredPlatform)
498
+ .values(row)
499
+ .onConflictDoUpdate({ target: foundryDeclaredPlatform.id, set: row });
500
+ },
501
+ async getServiceIds() {
502
+ const [row] = await db
503
+ .select({ serviceIds: foundryDeclaredPlatform.serviceIds })
504
+ .from(foundryDeclaredPlatform)
505
+ .limit(1);
506
+ return row?.serviceIds ?? {};
507
+ },
508
+ async putServiceIds(serviceIds) {
509
+ await db.update(foundryDeclaredPlatform).set({ serviceIds });
510
+ },
511
+ };
512
+ }
513
+ /**
514
+ * `value`'s own field-specific payload, stripped of its `field` -- that is
515
+ * a column of its own (`foundryRequestedChanges.field`), not repeated
516
+ * inside the jsonb.
517
+ */
518
+ function requestedChangePayload(value) {
519
+ const { field, ...payload } = value;
520
+ return payload;
521
+ }
522
+ function rowToRequestedChange(row) {
523
+ return {
524
+ id: row.id,
525
+ orgId: row.orgId,
526
+ value: { field: row.field, ...row.value },
527
+ requestedBy: row.requestedBy,
528
+ createdAt: row.createdAt.toISOString(),
529
+ state: row.state,
530
+ };
531
+ }
532
+ /**
533
+ * `RequestedChangeStore` (`../store.ts`) on Postgres, for #178: classify and
534
+ * store a company's own requested setting change on its declared org, beside
535
+ * `createPostgresDeclaredOrgStore` and `createPostgresPlatformStore` above.
536
+ * `create` classifies with `../requested-changes.js`'s `classifyChange`
537
+ * against the `org` given to it -- this store never loads the org itself,
538
+ * the same "caller already has it" choice `PlatformStore.put`'s `platform`
539
+ * argument makes -- and stores the result as the request's initial `state`.
540
+ */
541
+ export function createPostgresRequestedChangeStore(db) {
542
+ return {
543
+ async create({ orgId, org, value, requestedBy }) {
544
+ const classification = classifyChange(org, value);
545
+ const state = classification === 'auto' ? 'auto' : 'needs_approval';
546
+ const [row] = await db
547
+ .insert(foundryRequestedChanges)
548
+ .values({
549
+ orgId,
550
+ field: value.field,
551
+ value: requestedChangePayload(value),
552
+ requestedBy,
553
+ state,
554
+ })
555
+ .returning();
556
+ if (!row)
557
+ throw new RequestedChangeError('insert returned no row');
558
+ return rowToRequestedChange(row);
559
+ },
560
+ async get(id) {
561
+ const [row] = await db
562
+ .select()
563
+ .from(foundryRequestedChanges)
564
+ .where(eq(foundryRequestedChanges.id, id))
565
+ .limit(1);
566
+ return row ? rowToRequestedChange(row) : null;
567
+ },
568
+ async listForOrg(orgId) {
569
+ const rows = await db
570
+ .select()
571
+ .from(foundryRequestedChanges)
572
+ .where(eq(foundryRequestedChanges.orgId, orgId))
573
+ .orderBy(asc(foundryRequestedChanges.createdAt));
574
+ return rows.map(rowToRequestedChange);
575
+ },
576
+ };
577
+ }