forgeos 0.1.0-alpha.34 → 0.1.0-alpha.35

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/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.34 input=ce9cf8b6c553d7de8113eb2a388d269cb8c90d4bb15c013a7ab907ef207619b9 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.35 input=a41a4935f71e0376ad8546906f6e5bd7c01f6e033fc3aba669f636ad528e9881 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.35
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden field-test regressions found while building WorkOS-style multi-tenant apps.
8
+
9
+ - Resolve camelCase `ref:` targets such as `ref:accessRequests` to canonical SQL table names like `access_requests`, and report unknown refs before invalid SQL reaches PGlite.
10
+ - Allow WorkOS-like local dev auth headers through dev-server CORS, including organization, membership, permissions, roles, and claims headers.
11
+ - Teach the generated agent contract and capability map to recognize camelCase `ctx.db` aliases for snake_case tables.
12
+ - Avoid printing fake `http://127.0.0.1:0` API URLs in `forge dev --port 0` startup diagnostics.
13
+
3
14
  ## 0.1.0-alpha.34
4
15
 
5
16
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,20 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.35
10
+
11
+ - Hardened field-test regressions found while building WorkOS-style
12
+ multi-tenant apps.
13
+ - Resolved camelCase `ref:` targets such as `ref:accessRequests` to canonical
14
+ SQL table names like `access_requests`, and reported unknown refs before
15
+ invalid SQL reaches PGlite.
16
+ - Allowed WorkOS-like local dev auth headers through dev-server CORS,
17
+ including organization, membership, permissions, roles, and claims headers.
18
+ - Taught the generated agent contract and capability map to recognize
19
+ camelCase `ctx.db` aliases for snake_case tables.
20
+ - Avoided printing fake `http://127.0.0.1:0` API URLs in
21
+ `forge dev --port 0` startup diagnostics.
22
+
9
23
  ## 0.1.0-alpha.34
10
24
 
11
25
  - Hardened the ForgeOS app-building DX after the Vendor Access field test.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.34",
3
+ "version": "0.1.0-alpha.35",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.34","releaseId":"forgeos@0.1.0-alpha.34+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.35","releaseId":"forgeos@0.1.0-alpha.35+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.34 input=ce9cf8b6c553d7de8113eb2a388d269cb8c90d4bb15c013a7ab907ef207619b9 content=1a3f239bc322349796830461349167bb154a27e57fb1f10868c788194b3be97e
1
+ // @forge-generated generator=0.1.0-alpha.35 input=a41a4935f71e0376ad8546906f6e5bd7c01f6e033fc3aba669f636ad528e9881 content=55c7152fe779387c1acecdd5901f3a8affd1cfc9de8ef40b1c8883e083ebfa8a
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.34",
23
- "releaseId": "forgeos@0.1.0-alpha.34+unknown",
22
+ "packageVersion": "0.1.0-alpha.35",
23
+ "releaseId": "forgeos@0.1.0-alpha.35+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -595,6 +595,17 @@ export async function resolveAvailableWebPort(input: {
595
595
  };
596
596
  }
597
597
 
598
+ export function devStartupConsoleUrlOptions(input: {
599
+ host: string;
600
+ port: number;
601
+ webUrl?: string;
602
+ }): { apiUrl?: string; webUrl?: string } {
603
+ return {
604
+ ...(input.port === 0 ? {} : { apiUrl: `http://${input.host}:${input.port}` }),
605
+ ...(input.webUrl ? { webUrl: input.webUrl } : {}),
606
+ };
607
+ }
608
+
598
609
  export interface DevWatchGenerateFailureEvent {
599
610
  schemaVersion: "0.1.0";
600
611
  event: "dev.generate_failed";
@@ -1138,8 +1149,7 @@ export async function runDevCommand(
1138
1149
  mode: "startup",
1139
1150
  strictSecrets: false,
1140
1151
  includeImpact: true,
1141
- apiUrl: `http://${host}:${port}`,
1142
- ...(webUrl ? { webUrl } : {}),
1152
+ ...devStartupConsoleUrlOptions({ host, port, ...(webUrl ? { webUrl } : {}) }),
1143
1153
  });
1144
1154
  if (options.json) {
1145
1155
  process.stdout.write(formatDevConsoleJson(startupCycle));
@@ -283,9 +283,33 @@ function escapeRegExp(value: string): string {
283
283
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
284
284
  }
285
285
 
286
+ function toSnakeCaseName(name: string): string {
287
+ return name
288
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
289
+ .replace(/[-\s]+/g, "_")
290
+ .toLowerCase();
291
+ }
292
+
293
+ function toCamelCaseName(name: string): string {
294
+ return name.replace(/[_-]([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase());
295
+ }
296
+
297
+ function dbTableLookup(dataGraph: AgentContractInput["dataGraph"]): Map<string, string> {
298
+ const lookup = new Map<string, string>();
299
+ for (const table of dataGraph.tables) {
300
+ lookup.set(table.name, table.name);
301
+ lookup.set(table.exportName, table.name);
302
+ lookup.set(toSnakeCaseName(table.name), table.name);
303
+ lookup.set(toSnakeCaseName(table.exportName), table.name);
304
+ lookup.set(toCamelCaseName(table.name), table.name);
305
+ lookup.set(toCamelCaseName(table.exportName), table.name);
306
+ }
307
+ return lookup;
308
+ }
309
+
286
310
  function addDbAliasesForText(
287
311
  text: string,
288
- tableNames: Set<string>,
312
+ tableLookup: Map<string, string>,
289
313
  aliases: Map<string, string>,
290
314
  ): void {
291
315
  for (const match of text.matchAll(
@@ -293,8 +317,9 @@ function addDbAliasesForText(
293
317
  )) {
294
318
  const alias = match[1] ?? "";
295
319
  const table = match[2] ?? match[3] ?? "";
296
- if (alias && tableNames.has(table)) {
297
- aliases.set(alias, table);
320
+ const canonicalTable = tableLookup.get(table);
321
+ if (alias && canonicalTable) {
322
+ aliases.set(alias, canonicalTable);
298
323
  }
299
324
  }
300
325
 
@@ -304,8 +329,9 @@ function addDbAliasesForText(
304
329
  const [rawTable, rawAlias] = part.split(":").map((value) => value.trim());
305
330
  const table = rawTable?.replace(/["'`]/g, "") ?? "";
306
331
  const alias = (rawAlias ?? rawTable ?? "").replace(/\s*=.*$/, "").trim();
307
- if (tableNames.has(table) && alias) {
308
- aliases.set(alias, table);
332
+ const canonicalTable = tableLookup.get(table);
333
+ if (canonicalTable && alias) {
334
+ aliases.set(alias, canonicalTable);
309
335
  }
310
336
  }
311
337
  }
@@ -313,26 +339,28 @@ function addDbAliasesForText(
313
339
 
314
340
  function dbTablesForText(
315
341
  text: string,
316
- tableNames: Set<string>,
342
+ tableLookup: Map<string, string>,
317
343
  ops: Set<string>,
318
344
  ): string[] {
319
345
  const tables: string[] = [];
320
346
  for (const match of text.matchAll(/ctx\.db\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
321
347
  const table = match[1] ?? "";
322
348
  const op = match[2] ?? "";
323
- if (tableNames.has(table) && ops.has(op)) {
324
- tables.push(table);
349
+ const canonicalTable = tableLookup.get(table);
350
+ if (canonicalTable && ops.has(op)) {
351
+ tables.push(canonicalTable);
325
352
  }
326
353
  }
327
354
  for (const match of text.matchAll(/ctx\.db\s*\[\s*["'`]([^"'`]+)["'`]\s*\]\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
328
355
  const table = match[1] ?? "";
329
356
  const op = match[2] ?? "";
330
- if (tableNames.has(table) && ops.has(op)) {
331
- tables.push(table);
357
+ const canonicalTable = tableLookup.get(table);
358
+ if (canonicalTable && ops.has(op)) {
359
+ tables.push(canonicalTable);
332
360
  }
333
361
  }
334
362
  const aliases = new Map<string, string>();
335
- addDbAliasesForText(text, tableNames, aliases);
363
+ addDbAliasesForText(text, tableLookup, aliases);
336
364
  for (const [alias, table] of aliases) {
337
365
  const aliasPattern = new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*([A-Za-z_$][A-Za-z0-9_$]*)`, "g");
338
366
  for (const match of text.matchAll(aliasPattern)) {
@@ -348,10 +376,10 @@ function dbTablesForText(
348
376
  function dbTablesForFile(
349
377
  workspaceRoot: string,
350
378
  file: string | undefined,
351
- tableNames: Set<string>,
379
+ tableLookup: Map<string, string>,
352
380
  ops: Set<string>,
353
381
  ): string[] {
354
- return dbTablesForText(sourceTextWithLocalImports(workspaceRoot, file), tableNames, ops);
382
+ return dbTablesForText(sourceTextWithLocalImports(workspaceRoot, file), tableLookup, ops);
355
383
  }
356
384
 
357
385
  function emittedEventsForFile(workspaceRoot: string, file: string | undefined): string[] {
@@ -1222,7 +1250,7 @@ export function buildAgentContractArtifacts(
1222
1250
  );
1223
1251
 
1224
1252
  const runtimeEntries = new Map(input.runtimeGraph.entries.map((entry) => [entry.name, entry]));
1225
- const tableNames = new Set(input.dataGraph.tables.map((table) => table.name));
1253
+ const tableLookup = dbTableLookup(input.dataGraph);
1226
1254
  const externalEntries = input.externalServices?.services.flatMap((service) => service.entries) ?? [];
1227
1255
  const localCommandInfos: AgentContract["commands"] = sorted(Object.keys(input.apiSurface.commands), (name) => name).map((name) => {
1228
1256
  const entry = runtimeEntries.get(name);
@@ -1232,8 +1260,8 @@ export function buildAgentContractArtifacts(
1232
1260
  file,
1233
1261
  source: "local" as const,
1234
1262
  policy: authPolicy(commandAuth.get(name)),
1235
- tablesRead: dbTablesForFile(input.workspaceRoot, file, tableNames, DB_READ_OPS),
1236
- tablesWritten: dbTablesForFile(input.workspaceRoot, file, tableNames, DB_WRITE_OPS),
1263
+ tablesRead: dbTablesForFile(input.workspaceRoot, file, tableLookup, DB_READ_OPS),
1264
+ tablesWritten: dbTablesForFile(input.workspaceRoot, file, tableLookup, DB_WRITE_OPS),
1237
1265
  emits: emittedEventsForFile(input.workspaceRoot, file),
1238
1266
  allowedPackages: entry ? packageNamesForModule(input.appGraph, entry.moduleId) : [],
1239
1267
  forbiddenCapabilities: forbiddenForContext(input.classified, "command"),
@@ -1268,7 +1296,7 @@ export function buildAgentContractArtifacts(
1268
1296
  policy: authPolicy(queryAuth.get(query.name)),
1269
1297
  readOnly: true,
1270
1298
  tenantScoped: input.tenantScope.tables.length > 0,
1271
- tablesRead: dbTablesForFile(input.workspaceRoot, query.file, tableNames, DB_READ_OPS),
1299
+ tablesRead: dbTablesForFile(input.workspaceRoot, query.file, tableLookup, DB_READ_OPS),
1272
1300
  allowedPackages: packageNamesForModule(input.appGraph, query.moduleId),
1273
1301
  forbiddenCapabilities: forbiddenForContext(input.classified, "query"),
1274
1302
  http: httpEndpointFor("query", query.name),
@@ -1296,7 +1324,7 @@ export function buildAgentContractArtifacts(
1296
1324
  );
1297
1325
  const liveQueryInfos: AgentContract["liveQueries"] = sorted(input.liveQueryRegistry.liveQueries, (liveQuery) => liveQuery.name).map(
1298
1326
  (liveQuery) => {
1299
- const tablesRead = dbTablesForFile(input.workspaceRoot, liveQuery.file, tableNames, DB_READ_OPS);
1327
+ const tablesRead = dbTablesForFile(input.workspaceRoot, liveQuery.file, tableLookup, DB_READ_OPS);
1300
1328
  return {
1301
1329
  name: liveQuery.name,
1302
1330
  file: liveQuery.file,
@@ -102,39 +102,41 @@ function parseFieldType(
102
102
  field: DataField,
103
103
  tableName: string,
104
104
  diagnostics: Diagnostic[],
105
+ refTables: Map<string, string>,
105
106
  ): ParsedFieldType | null {
106
107
  const declared = field.type.trim();
107
108
  const nullable = declared.endsWith("?");
108
- const raw = (nullable ? declared.slice(0, -1) : declared).trim().toLowerCase();
109
+ const raw = (nullable ? declared.slice(0, -1) : declared).trim();
110
+ const lowered = raw.toLowerCase();
109
111
 
110
112
  const withNullability = (parsed: Omit<ParsedFieldType, "nullable">): ParsedFieldType => ({
111
113
  ...parsed,
112
114
  ...(nullable ? { nullable: true } : {}),
113
115
  });
114
116
 
115
- if (raw === "uuid") {
117
+ if (lowered === "uuid") {
116
118
  return withNullability({ sqlType: "uuid" });
117
119
  }
118
- if (raw === "text" || raw === "string") {
120
+ if (lowered === "text" || lowered === "string") {
119
121
  return withNullability({ sqlType: "text" });
120
122
  }
121
- if (raw === "number") {
123
+ if (lowered === "number") {
122
124
  return withNullability({ sqlType: "double precision" });
123
125
  }
124
- if (raw === "integer" || raw === "int") {
126
+ if (lowered === "integer" || lowered === "int") {
125
127
  return withNullability({ sqlType: "integer" });
126
128
  }
127
- if (raw === "boolean" || raw === "bool") {
129
+ if (lowered === "boolean" || lowered === "bool") {
128
130
  return withNullability({ sqlType: "boolean" });
129
131
  }
130
- if (raw === "timestamp" || raw === "timestamptz") {
132
+ if (lowered === "timestamp" || lowered === "timestamptz") {
131
133
  return withNullability({ sqlType: "timestamptz" });
132
134
  }
133
- if (raw === "json") {
135
+ if (lowered === "json") {
134
136
  return withNullability({ sqlType: "jsonb" });
135
137
  }
136
138
 
137
- if (raw.startsWith("enum:")) {
139
+ if (lowered.startsWith("enum:")) {
138
140
  const values = raw
139
141
  .slice("enum:".length)
140
142
  .split(",")
@@ -158,11 +160,11 @@ function parseFieldType(
158
160
  };
159
161
  }
160
162
 
161
- if (raw.startsWith("enum_")) {
163
+ if (lowered.startsWith("enum_")) {
162
164
  return withNullability({ sqlType: "text" });
163
165
  }
164
166
 
165
- if (raw.startsWith("ref:")) {
167
+ if (lowered.startsWith("ref:")) {
166
168
  const refTable = raw.slice("ref:".length).trim();
167
169
  if (!refTable) {
168
170
  diagnostics.push(
@@ -174,14 +176,25 @@ function parseFieldType(
174
176
  );
175
177
  return null;
176
178
  }
179
+ const resolvedRefTable = refTables.get(refTable) ?? refTables.get(toSnakeCase(refTable));
180
+ if (!resolvedRefTable) {
181
+ diagnostics.push(
182
+ createDiagnostic({
183
+ severity: "error",
184
+ code: FORGE_DB_INVALID_SQL_PLAN,
185
+ message: `unknown ref target '${refTable}' on ${tableName}.${field.name}; expected one of ${[...new Set(refTables.values())].sort().join(", ")}`,
186
+ }),
187
+ );
188
+ return null;
189
+ }
177
190
  return {
178
191
  ...(nullable ? { nullable: true } : {}),
179
192
  sqlType: "uuid",
180
- references: { table: toSnakeCase(refTable), column: "id" },
193
+ references: { table: resolvedRefTable, column: "id" },
181
194
  };
182
195
  }
183
196
 
184
- if (raw === "ref") {
197
+ if (lowered === "ref") {
185
198
  return {
186
199
  ...(nullable ? { nullable: true } : {}),
187
200
  sqlType: "uuid",
@@ -199,6 +212,18 @@ function parseFieldType(
199
212
  return null;
200
213
  }
201
214
 
215
+ function buildReferenceTableLookup(tables: DataTable[]): Map<string, string> {
216
+ const lookup = new Map<string, string>();
217
+ for (const table of tables) {
218
+ const sqlTableName = toSnakeCase(table.name);
219
+ lookup.set(table.name, sqlTableName);
220
+ lookup.set(table.exportName, sqlTableName);
221
+ lookup.set(sqlTableName, sqlTableName);
222
+ lookup.set(toSnakeCase(table.exportName), sqlTableName);
223
+ }
224
+ return lookup;
225
+ }
226
+
202
227
  function defaultExprForField(field: DataField): string | undefined {
203
228
  const name = field.name;
204
229
  const type = field.type.trim().replace(/\?$/, "").toLowerCase();
@@ -217,7 +242,11 @@ function defaultExprForField(field: DataField): string | undefined {
217
242
  return undefined;
218
243
  }
219
244
 
220
- function buildColumns(table: DataTable, diagnostics: Diagnostic[]): ColumnDef[] {
245
+ function buildColumns(
246
+ table: DataTable,
247
+ diagnostics: Diagnostic[],
248
+ refTables: Map<string, string>,
249
+ ): ColumnDef[] {
221
250
  const columns: ColumnDef[] = [];
222
251
  const fields = [...table.fields].sort((a, b) =>
223
252
  a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
@@ -236,7 +265,7 @@ function buildColumns(table: DataTable, diagnostics: Diagnostic[]): ColumnDef[]
236
265
  }
237
266
 
238
267
  for (const field of fields) {
239
- const parsed = parseFieldType(field, table.name, diagnostics);
268
+ const parsed = parseFieldType(field, table.name, diagnostics, refTables);
240
269
  if (!parsed) {
241
270
  continue;
242
271
  }
@@ -311,9 +340,13 @@ function buildForeignKeyIndexes(tableName: string, columns: ColumnDef[]): IndexD
311
340
  return indexes;
312
341
  }
313
342
 
314
- function buildTableChange(table: DataTable, diagnostics: Diagnostic[]): SqlChange | null {
343
+ function buildTableChange(
344
+ table: DataTable,
345
+ diagnostics: Diagnostic[],
346
+ refTables: Map<string, string>,
347
+ ): SqlChange | null {
315
348
  const tableName = toSnakeCase(table.name);
316
- const columns = buildColumns(table, diagnostics);
349
+ const columns = buildColumns(table, diagnostics, refTables);
317
350
 
318
351
  if (columns.length === 0) {
319
352
  diagnostics.push(
@@ -351,6 +384,7 @@ export function buildSqlPlan(dataGraph: DataGraph): SqlPlan {
351
384
  const diagnostics: Diagnostic[] = [];
352
385
  const tables: SqlChange[] = [];
353
386
  const indexes: SqlChange[] = [];
387
+ const refTables = buildReferenceTableLookup(dataGraph.tables);
354
388
 
355
389
  const systemTables: SqlChange[] = [
356
390
  {
@@ -406,7 +440,7 @@ export function buildSqlPlan(dataGraph: DataGraph): SqlPlan {
406
440
  ];
407
441
 
408
442
  for (const table of stableSortTables(dataGraph.tables)) {
409
- const change = buildTableChange(table, diagnostics);
443
+ const change = buildTableChange(table, diagnostics, refTables);
410
444
  if (!change) {
411
445
  continue;
412
446
  }
@@ -333,12 +333,27 @@ function markdownResponse(body: string, status = 200): Response {
333
333
  }
334
334
 
335
335
  function corsPreflight(): Response {
336
+ const allowedHeaders = [
337
+ "Authorization",
338
+ "Content-Type",
339
+ "WorkOS-Signature",
340
+ "x-forge-user-id",
341
+ "x-forge-tenant-id",
342
+ "x-forge-organization-id",
343
+ "x-forge-organization-membership-id",
344
+ "x-forge-role",
345
+ "x-forge-roles",
346
+ "x-forge-permissions",
347
+ "x-forge-claims",
348
+ "x-forge-auth-kind",
349
+ "x-forge-trace-id",
350
+ ];
336
351
  return new Response(null, {
337
352
  status: 204,
338
353
  headers: {
339
354
  "Access-Control-Allow-Origin": "*",
340
355
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
341
- "Access-Control-Allow-Headers": "Authorization, Content-Type, WorkOS-Signature, x-forge-user-id, x-forge-tenant-id, x-forge-role, x-forge-roles, x-forge-permissions",
356
+ "Access-Control-Allow-Headers": allowedHeaders.join(", "),
342
357
  },
343
358
  });
344
359
  }
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.34";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.35";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;