latticesql 1.13.4 → 1.13.6

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/cli.js CHANGED
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/cli.ts
10
- import { resolve as resolve9, dirname as dirname7 } from "path";
10
+ import { resolve as resolve9, dirname as dirname8 } from "path";
11
11
  import { readFileSync as readFileSync12 } from "fs";
12
12
  import { execSync } from "child_process";
13
13
  import { parse as parse2 } from "yaml";
@@ -189,6 +189,23 @@ function saveDbCredential(label, url) {
189
189
  creds[label] = url;
190
190
  saveCredentials(creds);
191
191
  }
192
+ function sanitizeTeamLabel(teamName) {
193
+ const stripped = teamName.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]+/g, "");
194
+ if (stripped.length === 0) return "team";
195
+ return stripped.startsWith(".") ? `team-${stripped}` : stripped;
196
+ }
197
+ function saveDbCredentialForTeam(opts) {
198
+ const base = sanitizeTeamLabel(opts.teamName);
199
+ let label = `${base}.config`;
200
+ const creds = loadCredentials();
201
+ if (label in creds && creds[label] !== opts.cloudUrl) {
202
+ const shortId = opts.teamId.split("-")[0] ?? opts.teamId.slice(0, 8);
203
+ label = `${base}-${shortId}.config`;
204
+ }
205
+ creds[label] = opts.cloudUrl;
206
+ saveCredentials(creds);
207
+ return label;
208
+ }
192
209
  var KEYS_SUBDIR = "keys";
193
210
  var TOKEN_EXT = ".token";
194
211
  function ensureKeysDir() {
@@ -4962,8 +4979,8 @@ async function checkForUpdate(pkgName, currentVersion) {
4962
4979
  // src/gui/server.ts
4963
4980
  import { createServer } from "http";
4964
4981
  import { spawn } from "child_process";
4965
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync11, readdirSync as readdirSync6, writeFileSync as writeFileSync6 } from "fs";
4966
- import { basename as basename6, dirname as dirname6, join as join13, resolve as resolve6, sep as sep3 } from "path";
4982
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
4983
+ import { basename as basename6, dirname as dirname7, join as join14, resolve as resolve6, sep as sep3 } from "path";
4967
4984
  import { parseDocument as parseDocument2 } from "yaml";
4968
4985
 
4969
4986
  // src/gui/data.ts
@@ -7613,8 +7630,10 @@ var guiAppHtml = `<!doctype html>
7613
7630
  // enters per-team things (cloud URL + team name) in this modal.
7614
7631
  fetchJson('/api/userconfig/identity').then(function (id) {
7615
7632
  var bodyHtml =
7616
- '<div class="field"><label>Cloud URL</label><input name="cloud_url" placeholder="http://localhost:4317" /></div>' +
7617
- '<div class="field"><label>Your email</label><input name="email" value="' + escapeHtml(id.email || '') + '" /></div>' +
7633
+ '<div class="field"><label>Cloud URL</label>' +
7634
+ '<input name="cloud_url" placeholder="postgres://postgres.&lt;ref&gt;:password@aws-x-region.pooler.supabase.com:5432/postgres" autocapitalize="off" autocorrect="off" spellcheck="false" />' +
7635
+ '</div>' +
7636
+ '<div class="field"><label>Your email</label><input name="email" value="' + escapeHtml(id.email || '') + '" autocapitalize="off" /></div>' +
7618
7637
  '<div class="field"><label>Your display name</label><input name="user_name" value="' + escapeHtml(id.display_name || '') + '" /></div>' +
7619
7638
  '<div class="field"><label>Team name</label><input name="team_name" /></div>' +
7620
7639
  '<p style="font-size:12px;color:var(--text-muted);margin:0">' +
@@ -7638,12 +7657,14 @@ var guiAppHtml = `<!doctype html>
7638
7657
  function showJoinTeamModal(kind) {
7639
7658
  fetchJson('/api/userconfig/identity').then(function (id) {
7640
7659
  var bodyHtml =
7641
- '<div class="field"><label>Cloud URL</label><input name="cloud_url" placeholder="http://localhost:4317" /></div>' +
7642
- '<div class="field"><label>Invite token</label><textarea name="invite_token" placeholder="latinv_..."></textarea></div>' +
7643
- '<div class="field"><label>Your email</label><input name="email" value="' + escapeHtml(id.email || '') + '" /></div>' +
7660
+ '<div class="field"><label>Cloud URL</label>' +
7661
+ '<input name="cloud_url" placeholder="postgres://postgres.&lt;ref&gt;:password@aws-x-region.pooler.supabase.com:5432/postgres" autocapitalize="off" autocorrect="off" spellcheck="false" />' +
7662
+ '</div>' +
7663
+ '<div class="field"><label>Invite token</label><textarea name="invite_token" placeholder="latinv_..." autocapitalize="off" autocorrect="off" spellcheck="false"></textarea></div>' +
7664
+ '<div class="field"><label>Your email</label><input name="email" value="' + escapeHtml(id.email || '') + '" autocapitalize="off" /></div>' +
7644
7665
  '<div class="field"><label>Your display name</label><input name="name" value="' + escapeHtml(id.display_name || '') + '" /></div>' +
7645
7666
  '<p style="font-size:12px;color:var(--text-muted);margin:0">' +
7646
- 'Email must match the address the invitation was addressed to.' +
7667
+ 'Use the same Postgres URL the inviter used (postgres://\u2026). Email must match the address the invitation was addressed to.' +
7647
7668
  '</p>';
7648
7669
  showModal('Join team', bodyHtml, {
7649
7670
  primaryLabel: 'Join',
@@ -9926,7 +9947,7 @@ async function handleDeleteRow(res, ctx, teamId, tableName, pk) {
9926
9947
  }
9927
9948
 
9928
9949
  // src/teams/client.ts
9929
- import { randomUUID } from "crypto";
9950
+ import { randomUUID as randomUUID2 } from "crypto";
9930
9951
 
9931
9952
  // src/framework/cloud-connect.ts
9932
9953
  async function probeCloud(targetUrl) {
@@ -10051,6 +10072,7 @@ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
10051
10072
  }
10052
10073
 
10053
10074
  // src/teams/direct-ops.ts
10075
+ import { randomUUID } from "crypto";
10054
10076
  async function listMembersDirect(db, teamId) {
10055
10077
  const members = await db.query("__lattice_team_members", {
10056
10078
  filters: [{ col: "team_id", op: "eq", val: teamId }]
@@ -10117,6 +10139,293 @@ async function destroyTeamDirect(db) {
10117
10139
  }
10118
10140
  await db.delete("__lattice_team_identity", "singleton");
10119
10141
  }
10142
+ async function redeemInviteDirect(cloudUrl, inviteToken, email, name) {
10143
+ if (!isPostgresUrl(cloudUrl)) {
10144
+ throw new Error(
10145
+ `redeemInviteDirect: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
10146
+ );
10147
+ }
10148
+ const db = new Lattice(cloudUrl);
10149
+ try {
10150
+ await db.init();
10151
+ for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
10152
+ await db.defineLate(table, def);
10153
+ }
10154
+ const invites = await db.query("__lattice_invitations", {
10155
+ filters: [
10156
+ { col: "token_hash", op: "eq", val: hashToken(inviteToken) },
10157
+ { col: "redeemed_at", op: "isNull" }
10158
+ ],
10159
+ limit: 1
10160
+ });
10161
+ const invite = invites[0];
10162
+ if (!invite) {
10163
+ throw new Error("Invitation invalid or already used");
10164
+ }
10165
+ if (invite.expires_at && new Date(invite.expires_at).getTime() < Date.now()) {
10166
+ throw new Error("Invitation expired");
10167
+ }
10168
+ if (invite.invitee_email && invite.invitee_email.toLowerCase() !== email.toLowerCase()) {
10169
+ throw new Error("Invitation is addressed to a different email");
10170
+ }
10171
+ const team = await db.get("__lattice_team", invite.team_id);
10172
+ if (!team || team.deleted_at) {
10173
+ throw new Error("Team no longer exists");
10174
+ }
10175
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10176
+ const userId = await db.insert("__lattice_users", {
10177
+ email,
10178
+ name,
10179
+ created_at: now,
10180
+ updated_at: now
10181
+ });
10182
+ await db.insert("__lattice_team_members", {
10183
+ team_id: invite.team_id,
10184
+ user_id: userId,
10185
+ role: "member",
10186
+ joined_at: now
10187
+ });
10188
+ const { raw, hash } = generateToken();
10189
+ await db.insert("__lattice_api_tokens", {
10190
+ user_id: userId,
10191
+ token_hash: hash,
10192
+ name: `invited:${team.name}`,
10193
+ created_at: now
10194
+ });
10195
+ await db.update("__lattice_invitations", invite.id, {
10196
+ redeemed_at: now,
10197
+ redeemed_by_user_id: userId
10198
+ });
10199
+ return {
10200
+ user: { id: userId, email, name },
10201
+ raw_token: raw,
10202
+ team: { id: team.id, name: team.name }
10203
+ };
10204
+ } finally {
10205
+ try {
10206
+ db.close();
10207
+ } catch {
10208
+ }
10209
+ }
10210
+ }
10211
+ async function openCloud(cloudUrl) {
10212
+ if (!isPostgresUrl(cloudUrl)) {
10213
+ throw new Error(
10214
+ `direct-ops: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
10215
+ );
10216
+ }
10217
+ const db = new Lattice(cloudUrl);
10218
+ await db.init();
10219
+ for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
10220
+ await db.defineLate(table, def);
10221
+ }
10222
+ return db;
10223
+ }
10224
+ function closeQuiet(db) {
10225
+ try {
10226
+ db.close();
10227
+ } catch {
10228
+ }
10229
+ }
10230
+ async function appendChangeEnvelopeDirect(db, args) {
10231
+ const existing = await db.query("__lattice_change_log", {
10232
+ filters: [{ col: "team_id", op: "eq", val: args.team_id }],
10233
+ orderBy: "seq",
10234
+ orderDir: "desc",
10235
+ limit: 1
10236
+ });
10237
+ const nextSeq = (existing[0]?.seq ?? 0) + 1;
10238
+ await db.insert("__lattice_change_log", {
10239
+ id: randomUUID(),
10240
+ seq: nextSeq,
10241
+ team_id: args.team_id,
10242
+ table_name: args.table_name,
10243
+ pk: args.pk,
10244
+ op: args.op,
10245
+ payload_json: args.payload_json,
10246
+ owner_user_id: args.owner_user_id,
10247
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
10248
+ });
10249
+ return nextSeq;
10250
+ }
10251
+ async function shareObjectDirect(cloudUrl, teamId, inviterUserId, table, spec) {
10252
+ const db = await openCloud(cloudUrl);
10253
+ try {
10254
+ const existing = await db.query("__lattice_shared_objects", {
10255
+ filters: [
10256
+ { col: "team_id", op: "eq", val: teamId },
10257
+ { col: "table_name", op: "eq", val: table }
10258
+ ],
10259
+ limit: 1
10260
+ });
10261
+ const prior = existing[0];
10262
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10263
+ let schemaVersion;
10264
+ let outSpec;
10265
+ if (prior && !prior.deleted_at) {
10266
+ schemaVersion = prior.schema_version + 1;
10267
+ outSpec = { ...spec, schemaVersion };
10268
+ await db.upsert("__lattice_shared_objects", {
10269
+ team_id: teamId,
10270
+ table_name: table,
10271
+ schema_spec_json: JSON.stringify(outSpec),
10272
+ schema_version: schemaVersion,
10273
+ created_by_user_id: prior.created_by_user_id,
10274
+ created_at: prior.created_at,
10275
+ updated_at: now,
10276
+ deleted_at: null
10277
+ });
10278
+ } else {
10279
+ schemaVersion = 1;
10280
+ outSpec = { ...spec, schemaVersion };
10281
+ await db.upsert("__lattice_shared_objects", {
10282
+ team_id: teamId,
10283
+ table_name: table,
10284
+ schema_spec_json: JSON.stringify(outSpec),
10285
+ schema_version: schemaVersion,
10286
+ created_by_user_id: inviterUserId,
10287
+ created_at: prior?.created_at ?? now,
10288
+ updated_at: now,
10289
+ deleted_at: null
10290
+ });
10291
+ }
10292
+ await applySchemaSpec(db, table, outSpec);
10293
+ const seq = await appendChangeEnvelopeDirect(db, {
10294
+ team_id: teamId,
10295
+ table_name: table,
10296
+ pk: null,
10297
+ op: "schema",
10298
+ payload_json: JSON.stringify(outSpec),
10299
+ owner_user_id: null
10300
+ });
10301
+ return { table, schema_version: schemaVersion, seq, schema_spec: outSpec };
10302
+ } finally {
10303
+ closeQuiet(db);
10304
+ }
10305
+ }
10306
+ async function listSharedObjectsDirect(cloudUrl, teamId) {
10307
+ const db = await openCloud(cloudUrl);
10308
+ try {
10309
+ const rows = await db.query("__lattice_shared_objects", {
10310
+ filters: [
10311
+ { col: "team_id", op: "eq", val: teamId },
10312
+ { col: "deleted_at", op: "isNull" }
10313
+ ]
10314
+ });
10315
+ return rows.map((r) => ({
10316
+ table: r.table_name,
10317
+ schema_version: r.schema_version,
10318
+ created_by_user_id: r.created_by_user_id,
10319
+ created_at: r.created_at,
10320
+ updated_at: r.updated_at,
10321
+ schema_spec: JSON.parse(r.schema_spec_json)
10322
+ }));
10323
+ } finally {
10324
+ closeQuiet(db);
10325
+ }
10326
+ }
10327
+ async function unshareObjectDirect(cloudUrl, teamId, table) {
10328
+ const db = await openCloud(cloudUrl);
10329
+ try {
10330
+ const existing = await db.query("__lattice_shared_objects", {
10331
+ filters: [
10332
+ { col: "team_id", op: "eq", val: teamId },
10333
+ { col: "table_name", op: "eq", val: table },
10334
+ { col: "deleted_at", op: "isNull" }
10335
+ ],
10336
+ limit: 1
10337
+ });
10338
+ const row = existing[0];
10339
+ if (!row) return;
10340
+ await db.upsert("__lattice_shared_objects", {
10341
+ team_id: teamId,
10342
+ table_name: table,
10343
+ schema_spec_json: row.schema_spec_json,
10344
+ schema_version: row.schema_version,
10345
+ created_by_user_id: row.created_by_user_id,
10346
+ created_at: row.created_at,
10347
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
10348
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString()
10349
+ });
10350
+ await appendChangeEnvelopeDirect(db, {
10351
+ team_id: teamId,
10352
+ table_name: table,
10353
+ pk: null,
10354
+ op: "unshare",
10355
+ payload_json: null,
10356
+ owner_user_id: null
10357
+ });
10358
+ } finally {
10359
+ closeQuiet(db);
10360
+ }
10361
+ }
10362
+ async function meDirect(cloudUrl, bearerToken) {
10363
+ const db = await openCloud(cloudUrl);
10364
+ try {
10365
+ const tokens = await db.query("__lattice_api_tokens", {
10366
+ filters: [{ col: "token_hash", op: "eq", val: hashToken(bearerToken) }],
10367
+ limit: 1
10368
+ });
10369
+ const tok = tokens[0];
10370
+ if (!tok || tok.revoked_at) throw new Error("Unauthorized");
10371
+ const user = await db.get("__lattice_users", tok.user_id);
10372
+ if (!user || user.deleted_at) throw new Error("Unauthorized");
10373
+ return { user: { id: user.id, email: user.email, name: user.name } };
10374
+ } finally {
10375
+ closeQuiet(db);
10376
+ }
10377
+ }
10378
+ async function linkRowDirect(local, cloudUrl, teamId, myUserId, table, pk) {
10379
+ const cloud = await openCloud(cloudUrl);
10380
+ let seq;
10381
+ try {
10382
+ await cloud.upsert("__lattice_row_links", {
10383
+ team_id: teamId,
10384
+ table_name: table,
10385
+ pk,
10386
+ owner_user_id: myUserId,
10387
+ linked_at: (/* @__PURE__ */ new Date()).toISOString()
10388
+ });
10389
+ seq = await appendChangeEnvelopeDirect(cloud, {
10390
+ team_id: teamId,
10391
+ table_name: table,
10392
+ pk,
10393
+ op: "link",
10394
+ payload_json: JSON.stringify({ owner_user_id: myUserId }),
10395
+ owner_user_id: myUserId
10396
+ });
10397
+ } finally {
10398
+ closeQuiet(cloud);
10399
+ }
10400
+ await local.upsert("__lattice_local_links", {
10401
+ team_id: teamId,
10402
+ table_name: table,
10403
+ pk,
10404
+ owner_user_id: myUserId,
10405
+ linked_at: (/* @__PURE__ */ new Date()).toISOString()
10406
+ });
10407
+ return { owner_user_id: myUserId, seq };
10408
+ }
10409
+ async function unlinkRowDirect(local, cloudUrl, teamId, table, pk) {
10410
+ const cloud = await openCloud(cloudUrl);
10411
+ try {
10412
+ await cloud.delete("__lattice_row_links", { team_id: teamId, table_name: table, pk });
10413
+ await appendChangeEnvelopeDirect(cloud, {
10414
+ team_id: teamId,
10415
+ table_name: table,
10416
+ pk,
10417
+ op: "unlink",
10418
+ payload_json: null,
10419
+ owner_user_id: null
10420
+ });
10421
+ } finally {
10422
+ closeQuiet(cloud);
10423
+ }
10424
+ try {
10425
+ await local.delete("__lattice_local_links", { team_id: teamId, table_name: table, pk });
10426
+ } catch {
10427
+ }
10428
+ }
10120
10429
 
10121
10430
  // src/teams/client.ts
10122
10431
  var TeamsClient = class {
@@ -10160,6 +10469,9 @@ var TeamsClient = class {
10160
10469
  });
10161
10470
  }
10162
10471
  async redeemInvite(cloudUrl, inviteToken, email, name) {
10472
+ if (isPostgresUrl(cloudUrl)) {
10473
+ return redeemInviteDirect(cloudUrl, inviteToken, email, name);
10474
+ }
10163
10475
  return this.fetchUnauthed(cloudUrl, "POST", "/api/auth/redeem-invite", {
10164
10476
  invite_token: inviteToken,
10165
10477
  email,
@@ -10315,6 +10627,9 @@ var TeamsClient = class {
10315
10627
  );
10316
10628
  }
10317
10629
  async me(cloudUrl, token) {
10630
+ if (isPostgresUrl(cloudUrl)) {
10631
+ return meDirect(cloudUrl, token);
10632
+ }
10318
10633
  return this.fetchAuthed(cloudUrl, token, "GET", "/api/auth/me");
10319
10634
  }
10320
10635
  // ── Object sharing (Phase 3) ────────────────────────────────────────────
@@ -10323,7 +10638,15 @@ var TeamsClient = class {
10323
10638
  * `schema_version` and replaces the stored spec — useful for evolving
10324
10639
  * shared schemas additively.
10325
10640
  */
10326
- async shareObject(cloudUrl, token, teamId, table, schemaSpec) {
10641
+ async shareObject(cloudUrl, token, teamId, table, schemaSpec, inviterUserId) {
10642
+ if (isPostgresUrl(cloudUrl)) {
10643
+ if (!inviterUserId) {
10644
+ throw new Error(
10645
+ "shareObject: inviterUserId is required for direct-Postgres cloud URLs (read it from __lattice_team_connections.my_user_id)"
10646
+ );
10647
+ }
10648
+ return shareObjectDirect(cloudUrl, teamId, inviterUserId, table, schemaSpec);
10649
+ }
10327
10650
  return this.fetchAuthed(
10328
10651
  cloudUrl,
10329
10652
  token,
@@ -10338,6 +10661,10 @@ var TeamsClient = class {
10338
10661
  * row and appends an `unshare` envelope to the change log.
10339
10662
  */
10340
10663
  async unshareObject(cloudUrl, token, teamId, table) {
10664
+ if (isPostgresUrl(cloudUrl)) {
10665
+ await unshareObjectDirect(cloudUrl, teamId, table);
10666
+ return;
10667
+ }
10341
10668
  await this.fetchAuthed(
10342
10669
  cloudUrl,
10343
10670
  token,
@@ -10346,6 +10673,9 @@ var TeamsClient = class {
10346
10673
  );
10347
10674
  }
10348
10675
  async listSharedObjects(cloudUrl, token, teamId) {
10676
+ if (isPostgresUrl(cloudUrl)) {
10677
+ return listSharedObjectsDirect(cloudUrl, teamId);
10678
+ }
10349
10679
  const r = await this.fetchAuthed(
10350
10680
  cloudUrl,
10351
10681
  token,
@@ -10485,6 +10815,18 @@ var TeamsClient = class {
10485
10815
  if (!snapshot) {
10486
10816
  throw new Error(`Row not found in local table "${table}" with pk "${pk}"`);
10487
10817
  }
10818
+ if (isPostgresUrl(connection.cloud_url)) {
10819
+ const result2 = await linkRowDirect(
10820
+ this.local,
10821
+ connection.cloud_url,
10822
+ connection.team_id,
10823
+ connection.my_user_id,
10824
+ table,
10825
+ pk
10826
+ );
10827
+ this.ensureWriteHook(table);
10828
+ return result2;
10829
+ }
10488
10830
  const result = await this.fetchAuthed(
10489
10831
  connection.cloud_url,
10490
10832
  connection.api_token,
@@ -10510,6 +10852,10 @@ var TeamsClient = class {
10510
10852
  */
10511
10853
  async unlinkRow(connection, table, pk) {
10512
10854
  await this.ensureLocalTables();
10855
+ if (isPostgresUrl(connection.cloud_url)) {
10856
+ await unlinkRowDirect(this.local, connection.cloud_url, connection.team_id, table, pk);
10857
+ return;
10858
+ }
10513
10859
  await this.fetchAuthed(
10514
10860
  connection.cloud_url,
10515
10861
  connection.api_token,
@@ -10585,7 +10931,7 @@ var TeamsClient = class {
10585
10931
  if (conn.my_user_id !== link.owner_user_id) continue;
10586
10932
  const now = (/* @__PURE__ */ new Date()).toISOString();
10587
10933
  await this.local.insert("__lattice_team_outbox", {
10588
- id: randomUUID(),
10934
+ id: randomUUID2(),
10589
10935
  team_id: link.team_id,
10590
10936
  table_name: ctx.table,
10591
10937
  pk: ctx.pk,
@@ -10611,6 +10957,9 @@ var TeamsClient = class {
10611
10957
  */
10612
10958
  async drainOutbox(connection) {
10613
10959
  await this.ensureLocalTables();
10960
+ if (isPostgresUrl(connection.cloud_url)) {
10961
+ return { pushed: 0, failed: 0 };
10962
+ }
10614
10963
  const now = (/* @__PURE__ */ new Date()).toISOString();
10615
10964
  const rows = await this.local.query("__lattice_team_outbox", {
10616
10965
  filters: [
@@ -10671,6 +11020,9 @@ var TeamsClient = class {
10671
11020
  */
10672
11021
  async pullChanges(connection, batchSize = 500) {
10673
11022
  await this.ensureLocalTables();
11023
+ if (isPostgresUrl(connection.cloud_url)) {
11024
+ return { applied: 0, last_seq: 0, dlq_count: 0 };
11025
+ }
10674
11026
  const connRow = await this.local.get(
10675
11027
  "__lattice_team_connections",
10676
11028
  connection.team_id
@@ -10694,7 +11046,7 @@ var TeamsClient = class {
10694
11046
  totalApplied++;
10695
11047
  } catch (e) {
10696
11048
  await this.local.insert("__lattice_team_dlq", {
10697
- id: randomUUID(),
11049
+ id: randomUUID2(),
10698
11050
  team_id: connection.team_id,
10699
11051
  envelope_json: JSON.stringify(env),
10700
11052
  error: e.message,
@@ -10836,6 +11188,23 @@ var TeamsHttpError = class extends Error {
10836
11188
  };
10837
11189
 
10838
11190
  // src/gui/teams-routes.ts
11191
+ import { existsSync as existsSync12, writeFileSync as writeFileSync5 } from "fs";
11192
+ import { dirname as dirname5, join as join12 } from "path";
11193
+ function writeTeamConfigYaml(activeConfigPath, credentialLabel, teamName) {
11194
+ const projectDir = dirname5(activeConfigPath);
11195
+ const yamlPath = join12(projectDir, `${credentialLabel}.yml`);
11196
+ if (existsSync12(yamlPath)) return yamlPath;
11197
+ const yaml = `# Joined-team config \u2014 managed by lattice gui. Edit entities: to add
11198
+ # locally-projected tables of the team's shared data; the cloud DB at
11199
+ # ${credentialLabel} is the authoritative source.
11200
+ db: \${LATTICE_DB:${credentialLabel}}
11201
+
11202
+ # Team: ${teamName.replace(/[\r\n]/g, " ")}
11203
+ entities: {}
11204
+ `;
11205
+ writeFileSync5(yamlPath, yaml, "utf8");
11206
+ return yamlPath;
11207
+ }
10839
11208
  function sendJson2(res, body, status = 200) {
10840
11209
  res.writeHead(status, {
10841
11210
  "content-type": "application/json; charset=utf-8",
@@ -11008,7 +11377,8 @@ async function dispatchTeamSubroute(req, res, ctx, teamId, subpath) {
11008
11377
  conn.api_token,
11009
11378
  teamId,
11010
11379
  table,
11011
- spec
11380
+ spec,
11381
+ conn.my_user_id
11012
11382
  );
11013
11383
  sendJson2(res, result);
11014
11384
  return;
@@ -11060,7 +11430,19 @@ async function handleJoin(req, res, ctx) {
11060
11430
  my_user_id: result.user.id,
11061
11431
  api_token: result.raw_token
11062
11432
  });
11063
- sendJson2(res, { ok: true, team: result.team, user: result.user });
11433
+ const credentialLabel = saveDbCredentialForTeam({
11434
+ teamName: result.team.name,
11435
+ teamId: result.team.id,
11436
+ cloudUrl
11437
+ });
11438
+ const configYamlPath = writeTeamConfigYaml(ctx.configPath, credentialLabel, result.team.name);
11439
+ sendJson2(res, {
11440
+ ok: true,
11441
+ team: result.team,
11442
+ user: result.user,
11443
+ credential_label: credentialLabel,
11444
+ config_path: configYamlPath
11445
+ });
11064
11446
  }
11065
11447
  async function handleRegisterAndCreate(req, res, ctx) {
11066
11448
  const body = await readJson2(req);
@@ -11108,8 +11490,8 @@ async function handleLeave(res, ctx, teamId) {
11108
11490
  }
11109
11491
 
11110
11492
  // src/gui/userconfig-routes.ts
11111
- import { existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
11112
- import { basename as basename4, dirname as dirname5, join as join12 } from "path";
11493
+ import { existsSync as existsSync13, readdirSync as readdirSync5 } from "fs";
11494
+ import { basename as basename4, dirname as dirname6, join as join13 } from "path";
11113
11495
  function sendJson3(res, body, status = 200) {
11114
11496
  res.writeHead(status, {
11115
11497
  "content-type": "application/json; charset=utf-8",
@@ -11161,12 +11543,12 @@ async function upsertIdentityRow(db, identity) {
11161
11543
  }
11162
11544
  }
11163
11545
  function listProjectConfigs(activeConfigPath) {
11164
- const dir = dirname5(activeConfigPath);
11546
+ const dir = dirname6(activeConfigPath);
11165
11547
  const out = [];
11166
- if (!existsSync12(dir)) return out;
11548
+ if (!existsSync13(dir)) return out;
11167
11549
  for (const fname of readdirSync5(dir)) {
11168
11550
  if (!fname.endsWith(".yml") && !fname.endsWith(".yaml")) continue;
11169
- const full = join12(dir, fname);
11551
+ const full = join13(dir, fname);
11170
11552
  try {
11171
11553
  const parsed = parseConfigFile(full);
11172
11554
  out.push({
@@ -11231,12 +11613,12 @@ async function dispatchUserConfigRoute(req, res, ctx) {
11231
11613
  }
11232
11614
 
11233
11615
  // src/gui/dbconfig-routes.ts
11234
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
11616
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
11235
11617
  import { basename as basename5, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve5 } from "path";
11236
11618
  import { parseDocument } from "yaml";
11237
11619
 
11238
11620
  // src/framework/cloud-migration.ts
11239
- import { existsSync as existsSync13, renameSync as renameSync2, unlinkSync as unlinkSync4 } from "fs";
11621
+ import { existsSync as existsSync14, renameSync as renameSync2, unlinkSync as unlinkSync4 } from "fs";
11240
11622
 
11241
11623
  // src/framework/native-entities.ts
11242
11624
  var NATIVE_ENTITY_DEFS = {
@@ -11343,14 +11725,14 @@ async function openTargetLatticeForMigration(configPath, targetUrl, encryptionKe
11343
11725
  return target;
11344
11726
  }
11345
11727
  function archiveLocalSqlite(dbPath) {
11346
- if (!existsSync13(dbPath)) {
11728
+ if (!existsSync14(dbPath)) {
11347
11729
  throw new Error(`archiveLocalSqlite: source file does not exist: ${dbPath}`);
11348
11730
  }
11349
11731
  const backupPath = `${dbPath}.local-bak`;
11350
11732
  const siblings = ["", "-shm", "-wal"];
11351
11733
  for (const suffix of siblings) {
11352
11734
  const stale = `${dbPath}.local-bak${suffix}`;
11353
- if (existsSync13(stale)) {
11735
+ if (existsSync14(stale)) {
11354
11736
  try {
11355
11737
  unlinkSync4(stale);
11356
11738
  } catch {
@@ -11359,7 +11741,7 @@ function archiveLocalSqlite(dbPath) {
11359
11741
  }
11360
11742
  for (const suffix of siblings) {
11361
11743
  const src = `${dbPath}${suffix}`;
11362
- if (!existsSync13(src)) continue;
11744
+ if (!existsSync14(src)) continue;
11363
11745
  const dest = `${dbPath}.local-bak${suffix}`;
11364
11746
  renameSync2(src, dest);
11365
11747
  }
@@ -11528,7 +11910,7 @@ async function detectTeamEnabled(db) {
11528
11910
  function rewriteDbLine(configPath, newValue) {
11529
11911
  const doc = parseDocument(readFileSync10(configPath, "utf8"));
11530
11912
  doc.set("db", newValue);
11531
- writeFileSync5(configPath, doc.toString(), "utf8");
11913
+ writeFileSync6(configPath, doc.toString(), "utf8");
11532
11914
  }
11533
11915
  function parseSaveBody(body) {
11534
11916
  const type = body.type;
@@ -11999,9 +12381,9 @@ function readRowContext(outputDir, locator, secretCols) {
11999
12381
  throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
12000
12382
  }
12001
12383
  return fileNames.map((filename) => {
12002
- const absPath = join13(entityDir, filename);
12003
- const relPath = join13(directoryRoot, slug, filename);
12004
- if (!existsSync14(absPath)) return { name: filename, path: relPath, content: "" };
12384
+ const absPath = join14(entityDir, filename);
12385
+ const relPath = join14(directoryRoot, slug, filename);
12386
+ if (!existsSync15(absPath)) return { name: filename, path: relPath, content: "" };
12005
12387
  let content = readFileSync11(absPath, "utf8");
12006
12388
  for (const col of secretCols) {
12007
12389
  const re = new RegExp(`^(${col}):.*$`, "gm");
@@ -12012,7 +12394,7 @@ function readRowContext(outputDir, locator, secretCols) {
12012
12394
  }
12013
12395
  async function openConfig(configPath, outputDir) {
12014
12396
  const parsed = parseConfigFile(configPath);
12015
- mkdirSync6(dirname6(parsed.dbPath), { recursive: true });
12397
+ mkdirSync6(dirname7(parsed.dbPath), { recursive: true });
12016
12398
  const encryptionKey = getOrCreateMasterKey();
12017
12399
  const db = new Lattice({ config: configPath }, { encryptionKey });
12018
12400
  registerNativeEntities(db);
@@ -12096,11 +12478,11 @@ async function openConfig(configPath, outputDir) {
12096
12478
  };
12097
12479
  }
12098
12480
  function listConfigs(activeConfigPath) {
12099
- const dir = dirname6(activeConfigPath);
12481
+ const dir = dirname7(activeConfigPath);
12100
12482
  const entries = [];
12101
12483
  for (const fname of readdirSync6(dir)) {
12102
12484
  if (!fname.endsWith(".yml") && !fname.endsWith(".yaml")) continue;
12103
- const full = join13(dir, fname);
12485
+ const full = join14(dir, fname);
12104
12486
  try {
12105
12487
  const parsed = parseConfigFile(full);
12106
12488
  entries.push({
@@ -12123,14 +12505,14 @@ function loadConfigDoc(configPath) {
12123
12505
  return parseDocument2(readFileSync11(configPath, "utf8"));
12124
12506
  }
12125
12507
  function saveConfigDoc(configPath, doc) {
12126
- writeFileSync6(configPath, doc.toString(), "utf8");
12508
+ writeFileSync7(configPath, doc.toString(), "utf8");
12127
12509
  }
12128
12510
  function createBlankConfig(activeConfigPath, dbName) {
12129
- const dir = dirname6(activeConfigPath);
12511
+ const dir = dirname7(activeConfigPath);
12130
12512
  const slug = dbName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
12131
12513
  if (!slug) throw new Error("Database name must contain at least one alphanumeric character");
12132
- const configPath = join13(dir, `${slug}.config.yml`);
12133
- if (existsSync14(configPath)) throw new Error(`Config already exists: ${slug}.config.yml`);
12514
+ const configPath = join14(dir, `${slug}.config.yml`);
12515
+ if (existsSync15(configPath)) throw new Error(`Config already exists: ${slug}.config.yml`);
12134
12516
  const yaml = `db: ./data/${slug}.db
12135
12517
 
12136
12518
  entities:
@@ -12142,8 +12524,8 @@ entities:
12142
12524
  deleted_at: { type: text }
12143
12525
  outputFile: ITEMS.md
12144
12526
  `;
12145
- writeFileSync6(configPath, yaml, "utf8");
12146
- mkdirSync6(join13(dir, "data"), { recursive: true });
12527
+ writeFileSync7(configPath, yaml, "utf8");
12528
+ mkdirSync6(join14(dir, "data"), { recursive: true });
12147
12529
  return configPath;
12148
12530
  }
12149
12531
  async function registerTeamCloudTables(db) {
@@ -12521,7 +12903,7 @@ async function startGuiServer(options) {
12521
12903
  return;
12522
12904
  }
12523
12905
  const newPath = resolve6(body.path);
12524
- if (!existsSync14(newPath)) {
12906
+ if (!existsSync15(newPath)) {
12525
12907
  sendJson5(res, { error: `Config not found: ${newPath}` }, 400);
12526
12908
  return;
12527
12909
  }
@@ -12723,6 +13105,7 @@ async function startGuiServer(options) {
12723
13105
  const handled = await dispatchTeamsGuiRoute(req, res, {
12724
13106
  db: active.db,
12725
13107
  client: active.teamsClient,
13108
+ configPath: active.configPath,
12726
13109
  pathname,
12727
13110
  method,
12728
13111
  validTables: active.validTables
@@ -12780,13 +13163,13 @@ async function startGuiServer(options) {
12780
13163
  }
12781
13164
 
12782
13165
  // src/gui/discover-output-dir.ts
12783
- import { existsSync as existsSync15 } from "fs";
12784
- import { join as join14, resolve as resolve7 } from "path";
13166
+ import { existsSync as existsSync16 } from "fs";
13167
+ import { join as join15, resolve as resolve7 } from "path";
12785
13168
  function discoverOutputDir(explicitOutput, explicit) {
12786
13169
  if (explicit) return explicitOutput;
12787
13170
  const candidates = ["./context", ".", "./generated"];
12788
13171
  for (const dir of candidates) {
12789
- if (existsSync15(join14(resolve7(dir), ".lattice", "manifest.json"))) return dir;
13172
+ if (existsSync16(join15(resolve7(dir), ".lattice", "manifest.json"))) return dir;
12790
13173
  }
12791
13174
  return explicitOutput;
12792
13175
  }
@@ -13504,7 +13887,7 @@ function runGenerate(args) {
13504
13887
  console.error('Error: config must have an "entities" key');
13505
13888
  process.exit(1);
13506
13889
  }
13507
- const configDir2 = dirname7(configPath);
13890
+ const configDir2 = dirname8(configPath);
13508
13891
  const outDir = resolve9(args.out);
13509
13892
  try {
13510
13893
  const result = generateAll({ config, configDir: configDir2, outDir, scaffold: args.scaffold });