@wtfalch/foundry 0.9.0 → 0.11.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 (44) hide show
  1. package/dist/apply.d.ts +16 -0
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.js +1 -1
  4. package/dist/migrations/0030_hosting_entry_hostnames.sql +24 -0
  5. package/dist/migrations/0031_adoptions.sql +37 -0
  6. package/dist/org.d.ts +49 -4
  7. package/dist/org.js +134 -1
  8. package/dist/plan/adopt.d.ts +112 -0
  9. package/dist/plan/adopt.js +252 -0
  10. package/dist/plan/adoption-vault.d.ts +9 -0
  11. package/dist/plan/adoption-vault.js +1 -0
  12. package/dist/plan/agora.d.ts +7 -0
  13. package/dist/plan/agora.js +9 -0
  14. package/dist/plan/broker-entries.d.ts +108 -0
  15. package/dist/plan/broker-entries.js +214 -0
  16. package/dist/plan/cloudflared-connector.d.ts +10 -2
  17. package/dist/plan/cloudflared-connector.js +24 -16
  18. package/dist/plan/diff.d.ts +14 -0
  19. package/dist/plan/diff.js +488 -47
  20. package/dist/plan/executor.d.ts +141 -3
  21. package/dist/plan/executor.js +1100 -149
  22. package/dist/plan/hostnames.d.ts +24 -0
  23. package/dist/plan/hostnames.js +56 -0
  24. package/dist/plan/index.d.ts +2 -0
  25. package/dist/plan/index.js +1 -0
  26. package/dist/plan/locked-hostnames.d.ts +8 -0
  27. package/dist/plan/locked-hostnames.js +26 -0
  28. package/dist/plan/login-host.d.ts +1 -1
  29. package/dist/plan/login-host.js +5 -2
  30. package/dist/plan/plan-apply.d.ts +63 -3
  31. package/dist/plan/plan-apply.js +163 -5
  32. package/dist/plan/platform-diff.d.ts +66 -13
  33. package/dist/plan/platform-diff.js +117 -15
  34. package/dist/plan/platform-providers.d.ts +95 -2
  35. package/dist/plan/platform-providers.js +393 -27
  36. package/dist/plan/providers-readers.d.ts +8 -1
  37. package/dist/plan/providers-readers.js +58 -4
  38. package/dist/plan.d.ts +6 -0
  39. package/dist/store/postgres.d.ts +6 -1
  40. package/dist/store/postgres.js +80 -5
  41. package/dist/store/schema.d.ts +162 -0
  42. package/dist/store/schema.js +19 -1
  43. package/dist/store.d.ts +37 -0
  44. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
- import { asc, eq, sql } from 'drizzle-orm';
2
- import { DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateDomain, validateHosting, validateMail, validateOwner, validateSite, } from '../org.js';
1
+ import { and, asc, eq, sql } from 'drizzle-orm';
2
+ import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateDomain, validateHosting, validateMail, validateOwner, validateSite, } from '../org.js';
3
+ import { lockedHostnames } from '../plan/locked-hostnames.js';
3
4
  import { validatePlatform } from '../platform.js';
4
5
  import { RequestedChangeError, classifyChange } from '../requested-changes.js';
5
6
  import { validateSlug } from '../slug.js';
6
- import { declaredOrgs, foundryAppliedState, foundryDeclaredPlatform, foundryRequestedChanges, } from './schema.js';
7
+ import { declaredOrgs, foundryAdoptions, foundryAppliedState, foundryDeclaredPlatform, foundryRequestedChanges, } from './schema.js';
7
8
  const UNIQUE_VIOLATION = '23505';
8
9
  /** `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. */
9
10
  const SLUG_UNIQUE_INDEX = 'foundry_declared_orgs_slug_idx';
@@ -81,7 +82,12 @@ function rowToOrg(row) {
81
82
  usesDomain: row.hostingUsesDomain,
82
83
  tunnel: row.hostingTunnel,
83
84
  issuerApps: row.hostingIssuerApps,
84
- entries: row.hostingEntries,
85
+ ...(row.hostingAdopt ? { adopt: row.hostingAdopt } : {}),
86
+ // Migration 0030's default for an entry stored before `hostnames`.
87
+ entries: row.hostingEntries.map((entry) => ({
88
+ ...entry,
89
+ hostnames: entry.hostnames ?? (entry.key === APP_ENTRY_KEY && row.hostingUsesDomain ? ['@'] : []),
90
+ })),
85
91
  },
86
92
  sso: { enabled: row.ssoEnabled },
87
93
  guests: { enabled: row.guestsEnabled },
@@ -121,6 +127,7 @@ function orgToRow(org) {
121
127
  hostingTunnel: org.hosting.tunnel,
122
128
  hostingIssuerApps: org.hosting.issuerApps,
123
129
  hostingEntries: org.hosting.entries,
130
+ hostingAdopt: org.hosting.adopt ?? null,
124
131
  ssoEnabled: org.sso.enabled,
125
132
  guestsEnabled: org.guests.enabled,
126
133
  soloEnabled: org.solo.enabled,
@@ -249,7 +256,10 @@ export function createPostgresDeclaredOrgStore(db) {
249
256
  // #71: same place `requireValidBuyer` checks the buyer identity (#67), on
250
257
  // both `create` and the merged result of `update`.
251
258
  function requireValidHosting(org) {
252
- const reason = validateHosting(org.hosting);
259
+ const reason = validateHosting(org.hosting, {
260
+ domain: org.domain,
261
+ locked: lockedHostnames(org, []),
262
+ });
253
263
  if (reason)
254
264
  throw new DeclaredOrgError(`hosting invalid: ${reason} (#71)`);
255
265
  }
@@ -604,3 +614,68 @@ export function createPostgresRequestedChangeStore(db) {
604
614
  },
605
615
  };
606
616
  }
617
+ function rowToAdoption(row) {
618
+ return {
619
+ orgId: row.orgId,
620
+ resourceKey: row.resourceKey,
621
+ kind: row.kind,
622
+ takenAt: row.takenAt.toISOString(),
623
+ settings: row.settings,
624
+ vaultRef: row.vaultRef,
625
+ state: row.state,
626
+ };
627
+ }
628
+ /**
629
+ * `AdoptionStore` (`../store.ts`) on Postgres, for #311: the snapshot row of
630
+ * each live resource foundry takes over, beside the stores above.
631
+ */
632
+ export function createPostgresAdoptionStore(db) {
633
+ return {
634
+ async put(adoption) {
635
+ const values = {
636
+ orgId: adoption.orgId,
637
+ resourceKey: adoption.resourceKey,
638
+ kind: adoption.kind,
639
+ takenAt: new Date(adoption.takenAt),
640
+ settings: adoption.settings,
641
+ vaultRef: adoption.vaultRef,
642
+ state: 'adopted',
643
+ };
644
+ await db
645
+ .insert(foundryAdoptions)
646
+ .values(values)
647
+ .onConflictDoUpdate({
648
+ target: [foundryAdoptions.orgId, foundryAdoptions.resourceKey],
649
+ set: values,
650
+ });
651
+ },
652
+ async get(orgId, resourceKey) {
653
+ const [row] = await db
654
+ .select()
655
+ .from(foundryAdoptions)
656
+ .where(and(eq(foundryAdoptions.orgId, orgId), eq(foundryAdoptions.resourceKey, resourceKey)))
657
+ .limit(1);
658
+ return row ? rowToAdoption(row) : null;
659
+ },
660
+ async list(orgId) {
661
+ const rows = await db
662
+ .select()
663
+ .from(foundryAdoptions)
664
+ .where(eq(foundryAdoptions.orgId, orgId))
665
+ .orderBy(asc(foundryAdoptions.resourceKey));
666
+ return rows.map(rowToAdoption);
667
+ },
668
+ async setState(orgId, resourceKey, state) {
669
+ await db
670
+ .update(foundryAdoptions)
671
+ .set({ state })
672
+ .where(and(eq(foundryAdoptions.orgId, orgId), eq(foundryAdoptions.resourceKey, resourceKey)));
673
+ },
674
+ async setSettings(orgId, resourceKey, settings) {
675
+ await db
676
+ .update(foundryAdoptions)
677
+ .set({ settings })
678
+ .where(and(eq(foundryAdoptions.orgId, orgId), eq(foundryAdoptions.resourceKey, resourceKey)));
679
+ },
680
+ };
681
+ }
@@ -482,6 +482,33 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
482
482
  }, {}, {
483
483
  $type: HostingEntry[];
484
484
  }>;
485
+ hostingAdopt: import("drizzle-orm/pg-core").PgColumn<{
486
+ name: "hosting_adopt";
487
+ tableName: "foundry_declared_orgs";
488
+ dataType: "json";
489
+ columnType: "PgJsonb";
490
+ data: {
491
+ database?: string;
492
+ tunnel?: string;
493
+ connectorApp?: string;
494
+ };
495
+ driverParam: unknown;
496
+ notNull: false;
497
+ hasDefault: false;
498
+ isPrimaryKey: false;
499
+ isAutoincrement: false;
500
+ hasRuntimeDefault: false;
501
+ enumValues: undefined;
502
+ baseColumn: never;
503
+ identity: undefined;
504
+ generated: undefined;
505
+ }, {}, {
506
+ $type: {
507
+ database?: string;
508
+ tunnel?: string;
509
+ connectorApp?: string;
510
+ };
511
+ }>;
485
512
  ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
486
513
  name: "sso_enabled";
487
514
  tableName: "foundry_declared_orgs";
@@ -1809,6 +1836,140 @@ export declare const foundryRequestedChanges: import("drizzle-orm/pg-core").PgTa
1809
1836
  };
1810
1837
  dialect: "pg";
1811
1838
  }>;
1839
+ /**
1840
+ * Mirrors `migrations/0031_adoptions.sql`, which is the source of truth (the
1841
+ * CHECKs live there). One row per live resource foundry took over (#311).
1842
+ * `settings` holds non-secret settings only; secrets go to the vault under
1843
+ * `vaultRef`.
1844
+ */
1845
+ export declare const foundryAdoptions: import("drizzle-orm/pg-core").PgTableWithColumns<{
1846
+ name: "foundry_adoptions";
1847
+ schema: undefined;
1848
+ columns: {
1849
+ orgId: import("drizzle-orm/pg-core").PgColumn<{
1850
+ name: "org_id";
1851
+ tableName: "foundry_adoptions";
1852
+ dataType: "string";
1853
+ columnType: "PgText";
1854
+ data: string;
1855
+ driverParam: string;
1856
+ notNull: true;
1857
+ hasDefault: false;
1858
+ isPrimaryKey: false;
1859
+ isAutoincrement: false;
1860
+ hasRuntimeDefault: false;
1861
+ enumValues: [string, ...string[]];
1862
+ baseColumn: never;
1863
+ identity: undefined;
1864
+ generated: undefined;
1865
+ }, {}, {}>;
1866
+ resourceKey: import("drizzle-orm/pg-core").PgColumn<{
1867
+ name: "resource_key";
1868
+ tableName: "foundry_adoptions";
1869
+ dataType: "string";
1870
+ columnType: "PgText";
1871
+ data: string;
1872
+ driverParam: string;
1873
+ notNull: true;
1874
+ hasDefault: false;
1875
+ isPrimaryKey: false;
1876
+ isAutoincrement: false;
1877
+ hasRuntimeDefault: false;
1878
+ enumValues: [string, ...string[]];
1879
+ baseColumn: never;
1880
+ identity: undefined;
1881
+ generated: undefined;
1882
+ }, {}, {}>;
1883
+ kind: import("drizzle-orm/pg-core").PgColumn<{
1884
+ name: "kind";
1885
+ tableName: "foundry_adoptions";
1886
+ dataType: "string";
1887
+ columnType: "PgText";
1888
+ data: string;
1889
+ driverParam: string;
1890
+ notNull: true;
1891
+ hasDefault: false;
1892
+ isPrimaryKey: false;
1893
+ isAutoincrement: false;
1894
+ hasRuntimeDefault: false;
1895
+ enumValues: [string, ...string[]];
1896
+ baseColumn: never;
1897
+ identity: undefined;
1898
+ generated: undefined;
1899
+ }, {}, {}>;
1900
+ takenAt: import("drizzle-orm/pg-core").PgColumn<{
1901
+ name: "taken_at";
1902
+ tableName: "foundry_adoptions";
1903
+ dataType: "date";
1904
+ columnType: "PgTimestamp";
1905
+ data: Date;
1906
+ driverParam: string;
1907
+ notNull: true;
1908
+ hasDefault: false;
1909
+ isPrimaryKey: false;
1910
+ isAutoincrement: false;
1911
+ hasRuntimeDefault: false;
1912
+ enumValues: undefined;
1913
+ baseColumn: never;
1914
+ identity: undefined;
1915
+ generated: undefined;
1916
+ }, {}, {}>;
1917
+ settings: import("drizzle-orm/pg-core").PgColumn<{
1918
+ name: "settings";
1919
+ tableName: "foundry_adoptions";
1920
+ dataType: "json";
1921
+ columnType: "PgJsonb";
1922
+ data: Record<string, unknown>;
1923
+ driverParam: unknown;
1924
+ notNull: true;
1925
+ hasDefault: false;
1926
+ isPrimaryKey: false;
1927
+ isAutoincrement: false;
1928
+ hasRuntimeDefault: false;
1929
+ enumValues: undefined;
1930
+ baseColumn: never;
1931
+ identity: undefined;
1932
+ generated: undefined;
1933
+ }, {}, {
1934
+ $type: Record<string, unknown>;
1935
+ }>;
1936
+ vaultRef: import("drizzle-orm/pg-core").PgColumn<{
1937
+ name: "vault_ref";
1938
+ tableName: "foundry_adoptions";
1939
+ dataType: "string";
1940
+ columnType: "PgText";
1941
+ data: string;
1942
+ driverParam: string;
1943
+ notNull: false;
1944
+ hasDefault: false;
1945
+ isPrimaryKey: false;
1946
+ isAutoincrement: false;
1947
+ hasRuntimeDefault: false;
1948
+ enumValues: [string, ...string[]];
1949
+ baseColumn: never;
1950
+ identity: undefined;
1951
+ generated: undefined;
1952
+ }, {}, {}>;
1953
+ state: import("drizzle-orm/pg-core").PgColumn<{
1954
+ name: "state";
1955
+ tableName: "foundry_adoptions";
1956
+ dataType: "string";
1957
+ columnType: "PgText";
1958
+ data: string;
1959
+ driverParam: string;
1960
+ notNull: true;
1961
+ hasDefault: true;
1962
+ isPrimaryKey: false;
1963
+ isAutoincrement: false;
1964
+ hasRuntimeDefault: false;
1965
+ enumValues: [string, ...string[]];
1966
+ baseColumn: never;
1967
+ identity: undefined;
1968
+ generated: undefined;
1969
+ }, {}, {}>;
1970
+ };
1971
+ dialect: "pg";
1972
+ }>;
1812
1973
  export type DeclaredOrgRow = typeof declaredOrgs.$inferSelect;
1813
1974
  export type NewDeclaredOrgRow = typeof declaredOrgs.$inferInsert;
1814
1975
  export type AppliedStateRow = typeof foundryAppliedState.$inferSelect;
@@ -1818,3 +1979,4 @@ export type DeclaredPlatformRow = typeof foundryDeclaredPlatform.$inferSelect;
1818
1979
  export type NewDeclaredPlatformRow = typeof foundryDeclaredPlatform.$inferInsert;
1819
1980
  export type RequestedChangeRow = typeof foundryRequestedChanges.$inferSelect;
1820
1981
  export type NewRequestedChangeRow = typeof foundryRequestedChanges.$inferInsert;
1982
+ export type AdoptionRow = typeof foundryAdoptions.$inferSelect;
@@ -1,4 +1,4 @@
1
- import { bigserial, boolean, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
1
+ import { bigserial, boolean, jsonb, pgTable, primaryKey, text, timestamp, } from 'drizzle-orm/pg-core';
2
2
  /**
3
3
  * Mirrors `migrations/0001_declared_org.sql`, which is the source of truth:
4
4
  * the SQL carries the CHECKs drizzle-kit does not generate here (this
@@ -54,6 +54,9 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
54
54
  // hosting_access_emails and hosting_app_env columns, which stay for an older
55
55
  // reader and are no longer written.
56
56
  hostingEntries: jsonb('hosting_entries').notNull().default([]).$type(),
57
+ // HostingConfig.adopt (org.ts): the live resources the org takes over by
58
+ // uuid. null when it adopts none. An entry's own `adopt` lives in `hostingEntries`.
59
+ hostingAdopt: jsonb('hosting_adopt').$type(),
57
60
  ssoEnabled: boolean('sso_enabled').notNull().default(false),
58
61
  guestsEnabled: boolean('guests_enabled').notNull().default(false),
59
62
  soloEnabled: boolean('solo_enabled').notNull().default(false),
@@ -172,3 +175,18 @@ export const foundryRequestedChanges = pgTable('foundry_requested_changes', {
172
175
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
173
176
  state: text('state').notNull().default('needs_approval'),
174
177
  });
178
+ /**
179
+ * Mirrors `migrations/0031_adoptions.sql`, which is the source of truth (the
180
+ * CHECKs live there). One row per live resource foundry took over (#311).
181
+ * `settings` holds non-secret settings only; secrets go to the vault under
182
+ * `vaultRef`.
183
+ */
184
+ export const foundryAdoptions = pgTable('foundry_adoptions', {
185
+ orgId: text('org_id').notNull(),
186
+ resourceKey: text('resource_key').notNull(),
187
+ kind: text('kind').notNull(),
188
+ takenAt: timestamp('taken_at', { withTimezone: true }).notNull(),
189
+ settings: jsonb('settings').notNull().$type(),
190
+ vaultRef: text('vault_ref'),
191
+ state: text('state').notNull().default('adopted'),
192
+ }, (table) => [primaryKey({ columns: [table.orgId, table.resourceKey] })]);
package/dist/store.d.ts CHANGED
@@ -96,3 +96,40 @@ export interface RequestedChangeStore {
96
96
  /** Every request an org has made, oldest first. */
97
97
  listForOrg(orgId: OrgId): Promise<RequestedChange[]>;
98
98
  }
99
+ export type AdoptionKind = 'app' | 'database' | 'tunnel' | 'connector' | 'access';
100
+ export type AdoptionState = 'adopted' | 'confirmed' | 'restored' | 'released';
101
+ /**
102
+ * What foundry recorded before it first changed a live resource it took over
103
+ * (#311). `settings` holds non-secret settings only; secrets live in the
104
+ * `AdoptionVault` (`plan/adoption-vault.ts`) under `vaultRef`.
105
+ */
106
+ export interface Adoption {
107
+ orgId: OrgId;
108
+ /** The scope key of the resource, for example `coolify:app:<orgId>:<entryKey>`. */
109
+ resourceKey: string;
110
+ kind: AdoptionKind;
111
+ /** ISO time the snapshot was taken. */
112
+ takenAt: string;
113
+ settings: Record<string, unknown>;
114
+ /** The vault ref of the secret part of the snapshot, or `null` when it has none. */
115
+ vaultRef: string | null;
116
+ state: AdoptionState;
117
+ }
118
+ /** The persistence seam for #311's {@link Adoption} rows, beside {@link DeclaredOrgStore}. */
119
+ export interface AdoptionStore {
120
+ /**
121
+ * Inserts the row, or replaces it in any state; the row is stored as
122
+ * `adopted`. A new snapshot after a restore or a release replaces the old one.
123
+ */
124
+ put(adoption: Omit<Adoption, 'state'>): Promise<void>;
125
+ get(orgId: OrgId, resourceKey: string): Promise<Adoption | null>;
126
+ /** Every row of an org, ordered by resource key. */
127
+ list(orgId: OrgId): Promise<Adoption[]>;
128
+ /** Moves a row to `state`. A no-op for a row that does not exist. */
129
+ setState(orgId: OrgId, resourceKey: string, state: AdoptionState): Promise<void>;
130
+ /**
131
+ * Replaces a row's `settings` and nothing else: its state, `takenAt` and
132
+ * `vaultRef` stay (#312). A no-op for a row that does not exist.
133
+ */
134
+ setSettings(orgId: OrgId, resourceKey: string, settings: Record<string, unknown>): Promise<void>;
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",