latticesql 1.13.2 → 1.13.3

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/README.md CHANGED
@@ -2086,6 +2086,10 @@ The convergence means you don't need to duplicate entity-context definitions in
2086
2086
 
2087
2087
  **Switch vs. migrate (v1.13.2+ wording).** "Connect to existing cloud" _switches_ the project's `db:` line to point at the cloud; the local SQLite file stays on disk and you can switch back by editing the YAML or via the Databases catalog under User Config. Use "Migrate to cloud" only when you want to _push_ the local data into a fresh empty target.
2088
2088
 
2089
+ **Upgrade to team cloud — works against direct Postgres URLs (v1.13.3+).** The "Upgrade to team cloud" action previously HTTP-POSTed to `/api/auth/register` on the cloud URL, which fails with `Request cannot be constructed from a URL that includes credentials` when the cloud URL is a Postgres connection string. v1.13.3 dispatches on URL scheme: `http(s)://` keeps the HTTP path; `postgres(ql)://` calls the new `registerDirectViaPostgres()` helper that drives the same INSERT sequence directly against the cloud Postgres. Same invariants enforced both ways.
2090
+
2091
+ **Dashboard renders every entity (v1.13.3+).** Previously the dashboard cards filtered through a hardcoded entity list (`meetings`, `people`, `messages`, `projects`, `repositories`, `files`). Installs whose YAML declared different names saw a blank dashboard. Now every first-class entity gets a card; the hardcoded list survives as an ordering preference only.
2092
+
2089
2093
  **Views**
2090
2094
 
2091
2095
  - **Dashboard** (`#/`) — one card per first-class entity with live row counts.
package/dist/cli.js CHANGED
@@ -6247,12 +6247,46 @@ var guiAppHtml = `<!doctype html>
6247
6247
  // Dashboard
6248
6248
  // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6249
6249
  function renderDashboard(content) {
6250
- var cards = DASHBOARD_ORDER.map(function (name) {
6251
- var t = tableByName(name);
6252
- if (!t) return '';
6253
- var d = displayFor(name);
6250
+ // Show every first-class (non-junction, non-system) entity. The
6251
+ // previous implementation used DASHBOARD_ORDER as the filter \u2014 meaning
6252
+ // installs whose YAML declared tables outside the hardcoded list
6253
+ // (e.g. clients / students / vendors) saw a blank dashboard with no
6254
+ // hint why. DASHBOARD_ORDER is now a preference for ordering only;
6255
+ // tables not in it appear after, in declaration order.
6256
+ var preferenceRank = function (name) {
6257
+ var idx = DASHBOARD_ORDER.indexOf(name);
6258
+ return idx === -1 ? DASHBOARD_ORDER.length : idx;
6259
+ };
6260
+ var firstClass = (state.entities.tables || [])
6261
+ .filter(function (t) {
6262
+ // Junctions belong on the Data Model page, not as dashboard cards.
6263
+ if (isJunction(t)) return false;
6264
+ // System tables (_lattice_gui_*, __lattice_*) are hidden.
6265
+ if (t.name.charAt(0) === '_') return false;
6266
+ return true;
6267
+ })
6268
+ .slice()
6269
+ .sort(function (a, b) {
6270
+ var ra = preferenceRank(a.name);
6271
+ var rb = preferenceRank(b.name);
6272
+ if (ra !== rb) return ra - rb;
6273
+ // Same preference rank \u2014 keep declaration order from the API.
6274
+ return 0;
6275
+ });
6276
+
6277
+ if (firstClass.length === 0) {
6278
+ content.innerHTML =
6279
+ '<div class="placeholder">' +
6280
+ '<h2>No entities yet</h2>' +
6281
+ '<p>Define entities in your <code>lattice.config.yml</code> or register them via <code>db.define()</code>, then reload.</p>' +
6282
+ '</div>';
6283
+ return;
6284
+ }
6285
+
6286
+ var cards = firstClass.map(function (t) {
6287
+ var d = displayFor(t.name);
6254
6288
  var count = (t.rowCount != null) ? t.rowCount : 0;
6255
- return '<a class="card" href="#/objects/' + name + '">' +
6289
+ return '<a class="card" href="#/objects/' + t.name + '">' +
6256
6290
  '<div class="card-icon">' + d.icon + '</div>' +
6257
6291
  '<div class="card-label">' + escapeHtml(d.label) + '</div>' +
6258
6292
  '<div class="card-count">' + count + '</div>' +
@@ -8035,6 +8069,81 @@ var guiAppHtml = `<!doctype html>
8035
8069
  };
8036
8070
  }
8037
8071
 
8072
+ // Detect common Supabase pooler URL mistakes the form gives no hint
8073
+ // about. Returns an array of human-readable hints, or [] when the
8074
+ // form looks plausible. Conservative \u2014 only flags clear patterns.
8075
+ function detectSupabasePoolerMistakes(body) {
8076
+ var hints = [];
8077
+ var host = (body.host || '').toLowerCase();
8078
+ if (host.indexOf('pooler.supabase') !== -1) {
8079
+ // Pooler requires the tenant-prefixed user form postgres.<ref>.
8080
+ if (body.user && body.user.indexOf('.') === -1) {
8081
+ hints.push(
8082
+ 'Supabase pooler hosts require a tenant-prefixed user like ' +
8083
+ '<code>postgres.&lt;project-ref&gt;</code>. You entered <code>' +
8084
+ escapeHtml(body.user) + '</code> \u2014 Supabase will reject SCRAM ' +
8085
+ 'auth with a misleading "password authentication failed" error.'
8086
+ );
8087
+ }
8088
+ // Session-mode is on 5432; transaction-mode on 6543. latticesql
8089
+ // wants session-mode (transactions span multiple statements).
8090
+ if (Number(body.port) === 6543) {
8091
+ hints.push(
8092
+ 'Supabase pooler port <code>6543</code> is transaction mode. ' +
8093
+ 'Lattice needs session mode \u2014 use port <code>5432</code> on ' +
8094
+ 'the same pooler host.'
8095
+ );
8096
+ }
8097
+ } else if (host.indexOf('.supabase.co') !== -1 && host.indexOf('pooler') === -1) {
8098
+ // Direct host form uses bare postgres user, not the tenant-
8099
+ // prefixed pooler form. Easy to mix up.
8100
+ if (body.user && body.user.indexOf('.') !== -1) {
8101
+ hints.push(
8102
+ 'The direct host <code>db.&lt;project-ref&gt;.supabase.co</code> ' +
8103
+ 'uses a bare <code>postgres</code> user (no tenant prefix). ' +
8104
+ 'You entered <code>' + escapeHtml(body.user) + '</code> \u2014 ' +
8105
+ 'Supabase will reject SCRAM auth with "password authentication ' +
8106
+ 'failed".'
8107
+ );
8108
+ }
8109
+ }
8110
+ return hints;
8111
+ }
8112
+
8113
+ // Probe the cloud and validate Supabase form patterns. Resolves to
8114
+ // the probe result on success; rejects with a human-readable error
8115
+ // when the form has obvious mistakes or the probe is unreachable.
8116
+ // Shared by Migrate + Connect so the credential is never saved
8117
+ // without first proving the form values can actually connect.
8118
+ function probeBeforeCredentialSave(body, msgEl) {
8119
+ var hints = detectSupabasePoolerMistakes(body);
8120
+ if (hints.length > 0) {
8121
+ // Block submit until the form is fixed. Show the hints inline.
8122
+ msgEl.innerHTML =
8123
+ '<strong style="color:var(--warn)">Connection looks wrong:</strong>' +
8124
+ '<ul style="margin:6px 0 0 18px;padding:0;color:var(--warn)">' +
8125
+ hints.map(function (h) { return '<li>' + h + '</li>'; }).join('') +
8126
+ '</ul>';
8127
+ return Promise.reject(new Error('Fix the issues above and try again.'));
8128
+ }
8129
+ msgEl.textContent = 'Testing connection\u2026';
8130
+ return fetch('/api/dbconfig/probe', {
8131
+ method: 'POST',
8132
+ headers: { 'content-type': 'application/json' },
8133
+ body: JSON.stringify(body),
8134
+ })
8135
+ .then(function (r) { return r.json(); })
8136
+ .then(function (probe) {
8137
+ if (!probe.reachable) {
8138
+ throw new Error(
8139
+ 'Cloud unreachable: ' + (probe.error || 'unknown error') +
8140
+ '. Double-check host, port, user, and password.'
8141
+ );
8142
+ }
8143
+ return probe;
8144
+ });
8145
+ }
8146
+
8038
8147
  function showMigrateToCloudModal(onClose) {
8039
8148
  var bodyHtml =
8040
8149
  '<p style="margin:0 0 12px;font-size:13px;color:var(--text-muted)">' +
@@ -8050,15 +8159,30 @@ var guiAppHtml = `<!doctype html>
8050
8159
  onSubmit: function () {
8051
8160
  var body = readPostgresWizardForm();
8052
8161
  var msg = document.getElementById('w-msg');
8053
- msg.textContent = 'Migrating\u2026 (this may take a moment for large DBs)';
8054
- return fetch('/api/dbconfig/migrate-to-cloud', {
8055
- method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
8056
- })
8057
- .then(function (r) { return r.json().then(function (d) { return { status: r.status, body: d }; }); })
8058
- .then(function (r) {
8059
- if (!r.body.ok) throw new Error(r.body.error || ('HTTP ' + r.status));
8060
- if (onClose) onClose();
8061
- });
8162
+ // Validate Supabase URL pattern + probe the cloud before
8163
+ // persisting a credential that would just blow up on the next
8164
+ // open. Saves users from the "Migrate succeeded, switch back
8165
+ // later fails" trap that strikes when the saved credential
8166
+ // has a wrong host/port/user.
8167
+ return probeBeforeCredentialSave(body, msg).then(function (probe) {
8168
+ if (probe.teamEnabled) {
8169
+ throw new Error(
8170
+ 'Target is already a teams DB' +
8171
+ (probe.teamName ? ' (' + probe.teamName + ')' : '') +
8172
+ '. Migrate-to-cloud only works against fresh empty targets \u2014 ' +
8173
+ 'use Connect to existing cloud instead.'
8174
+ );
8175
+ }
8176
+ msg.textContent = 'Migrating\u2026 (this may take a moment for large DBs)';
8177
+ return fetch('/api/dbconfig/migrate-to-cloud', {
8178
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
8179
+ })
8180
+ .then(function (r) { return r.json().then(function (d) { return { status: r.status, body: d }; }); })
8181
+ .then(function (r) {
8182
+ if (!r.body.ok) throw new Error(r.body.error || ('HTTP ' + r.status));
8183
+ if (onClose) onClose();
8184
+ });
8185
+ });
8062
8186
  },
8063
8187
  });
8064
8188
  }
@@ -8084,14 +8208,12 @@ var guiAppHtml = `<!doctype html>
8084
8208
  onSubmit: function () {
8085
8209
  var body = readPostgresWizardForm();
8086
8210
  var msg = document.getElementById('w-msg');
8087
- msg.textContent = 'Probing\u2026';
8088
- // First probe; if team, require token before the actual connect.
8089
- return fetch('/api/dbconfig/probe', {
8090
- method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
8091
- })
8092
- .then(function (r) { return r.json(); })
8211
+ // probeBeforeCredentialSave validates Supabase form patterns
8212
+ // before sending the probe; surfaces inline warnings (with
8213
+ // hints) when the user clearly has e.g. the wrong port or
8214
+ // missing tenant prefix in the pooler user.
8215
+ return probeBeforeCredentialSave(body, msg)
8093
8216
  .then(function (probe) {
8094
- if (!probe.reachable) throw new Error('Unreachable: ' + (probe.error || 'unknown'));
8095
8217
  if (probe.teamEnabled && !teamZoneShown) {
8096
8218
  var zone = document.getElementById('w-team-zone');
8097
8219
  zone.innerHTML =
@@ -9787,6 +9909,86 @@ async function probeCloud(targetUrl) {
9787
9909
  }
9788
9910
  }
9789
9911
 
9912
+ // src/teams/register-direct.ts
9913
+ function isPostgresUrl(url) {
9914
+ return /^postgres(ql)?:\/\//i.test(url);
9915
+ }
9916
+ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
9917
+ if (!isPostgresUrl(cloudUrl)) {
9918
+ throw new Error(
9919
+ `registerDirectViaPostgres: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
9920
+ );
9921
+ }
9922
+ const db = new Lattice(cloudUrl);
9923
+ try {
9924
+ await db.init();
9925
+ for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
9926
+ await db.defineLate(table, def);
9927
+ }
9928
+ const existing = await db.query("__lattice_users", {
9929
+ filters: [{ col: "deleted_at", op: "isNull" }],
9930
+ limit: 1
9931
+ });
9932
+ if (existing.length > 0) {
9933
+ throw new Error(
9934
+ "Registration is disabled. This cloud already has users \u2014 join via invitation."
9935
+ );
9936
+ }
9937
+ let identity = null;
9938
+ try {
9939
+ identity = await db.get("__lattice_team_identity", "singleton");
9940
+ } catch {
9941
+ identity = null;
9942
+ }
9943
+ if (identity) {
9944
+ throw new Error("This cloud already has a team. Use Connect to existing cloud instead.");
9945
+ }
9946
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9947
+ const userId = await db.insert("__lattice_users", {
9948
+ email,
9949
+ name,
9950
+ created_at: now,
9951
+ updated_at: now
9952
+ });
9953
+ const { raw, hash } = generateToken();
9954
+ await db.insert("__lattice_api_tokens", {
9955
+ user_id: userId,
9956
+ token_hash: hash,
9957
+ name: `creator:${teamName}`,
9958
+ created_at: now
9959
+ });
9960
+ const teamId = await db.insert("__lattice_team", {
9961
+ name: teamName,
9962
+ created_by_user_id: userId,
9963
+ created_at: now,
9964
+ updated_at: now
9965
+ });
9966
+ await db.insert("__lattice_team_members", {
9967
+ team_id: teamId,
9968
+ user_id: userId,
9969
+ role: "creator",
9970
+ joined_at: now
9971
+ });
9972
+ await db.insert("__lattice_team_identity", {
9973
+ id: "singleton",
9974
+ team_id: teamId,
9975
+ team_name: teamName,
9976
+ creator_email: email,
9977
+ created_at: now
9978
+ });
9979
+ return {
9980
+ user: { id: userId, email, name },
9981
+ raw_token: raw,
9982
+ team: { id: teamId, name: teamName, role: "creator" }
9983
+ };
9984
+ } finally {
9985
+ try {
9986
+ db.close();
9987
+ } catch {
9988
+ }
9989
+ }
9990
+ }
9991
+
9790
9992
  // src/teams/client.ts
9791
9993
  var TeamsClient = class {
9792
9994
  constructor(local) {
@@ -9882,15 +10084,22 @@ var TeamsClient = class {
9882
10084
  return { probe };
9883
10085
  }
9884
10086
  /**
9885
- * Upgrade an already-connected cloud DB to a team DB. Runs the
9886
- * atomic `POST /api/auth/register` flow against the cloud URL
9887
- * stored under `label`. Writes the resulting bearer to
9888
- * `~/.lattice/keys/<label>.token`. Caller is expected to call
9889
- * `saveConnection` if they also want the local
9890
- * `__lattice_team_connections` row populated.
10087
+ * Upgrade an already-connected cloud DB to a team DB. Two paths
10088
+ * depending on the cloud URL's scheme:
10089
+ *
10090
+ * - `http(s)://…` POST to the cloud's `/api/auth/register` endpoint
10091
+ * (`lattice serve --team-cloud` is fronting the Postgres).
10092
+ * - `postgres(ql)://…` drive the same INSERT sequence directly
10093
+ * against the cloud Postgres via {@link registerDirectViaPostgres}.
10094
+ * The HTTP path can't be used here because the browser's Fetch
10095
+ * API refuses URLs with embedded credentials.
10096
+ *
10097
+ * On success writes the bearer token to `~/.lattice/keys/<label>.token`.
10098
+ * Caller is expected to call `saveConnection` if they also want the
10099
+ * local `__lattice_team_connections` row populated.
9891
10100
  */
9892
10101
  async upgradeToTeamCloud(opts) {
9893
- const reg = await this.register(opts.cloudUrl, opts.email, opts.displayName, opts.teamName);
10102
+ const reg = isPostgresUrl(opts.cloudUrl) ? await registerDirectViaPostgres(opts.cloudUrl, opts.email, opts.displayName, opts.teamName) : await this.register(opts.cloudUrl, opts.email, opts.displayName, opts.teamName);
9894
10103
  writeToken(opts.label, reg.raw_token);
9895
10104
  return reg;
9896
10105
  }
@@ -11649,8 +11858,16 @@ async function openConfig(configPath, outputDir) {
11649
11858
  db.define("__lattice_user_identity", {
11650
11859
  columns: {
11651
11860
  id: "TEXT PRIMARY KEY",
11652
- display_name: 'TEXT NOT NULL DEFAULT ""',
11653
- email: 'TEXT NOT NULL DEFAULT ""',
11861
+ // Single-quoted empty-string defaults below — not double-quoted!
11862
+ // SQLite leniently accepts `DEFAULT ""` as an empty string literal,
11863
+ // but PostgreSQL treats `""` as a zero-length delimited identifier
11864
+ // (i.e. an empty column name), which throws `zero-length delimited
11865
+ // identifier at or near """""` from the parser before any rows are
11866
+ // inserted. This is the standard-conformant behavior — single
11867
+ // quotes are for string literals; double quotes are for
11868
+ // identifiers. Use `''` so the CREATE TABLE works on both engines.
11869
+ display_name: "TEXT NOT NULL DEFAULT ''",
11870
+ email: "TEXT NOT NULL DEFAULT ''",
11654
11871
  updated_at: "TEXT NOT NULL DEFAULT (datetime('now'))"
11655
11872
  },
11656
11873
  primaryKey: "id",
@@ -12126,7 +12343,20 @@ async function startGuiServer(options) {
12126
12343
  sendJson5(res, { error: `Config not found: ${newPath}` }, 400);
12127
12344
  return;
12128
12345
  }
12129
- const next = await openConfig(newPath, active.outputDir);
12346
+ let next;
12347
+ try {
12348
+ next = await openConfig(newPath, active.outputDir);
12349
+ } catch (e) {
12350
+ const err = e;
12351
+ console.error(`[dbconfig.switch] openConfig(${newPath}) failed:`, err);
12352
+ const codePrefix = err.code ? `[${err.code}] ` : "";
12353
+ sendJson5(
12354
+ res,
12355
+ { error: `Failed to switch to ${newPath}: ${codePrefix}${err.message}` },
12356
+ 500
12357
+ );
12358
+ return;
12359
+ }
12130
12360
  active.db.close();
12131
12361
  active = next;
12132
12362
  sendJson5(res, { ok: true, path: active.configPath });
package/dist/index.cjs CHANGED
@@ -64,6 +64,7 @@ __export(index_exports, {
64
64
  getOrCreateMasterKey: () => getOrCreateMasterKey,
65
65
  hashFile: () => hashFile,
66
66
  isEncrypted: () => isEncrypted,
67
+ isPostgresUrl: () => isPostgresUrl,
67
68
  isV1EntityFiles: () => isV1EntityFiles,
68
69
  listDbCredentials: () => listDbCredentials,
69
70
  listTokens: () => listTokens,
@@ -81,6 +82,7 @@ __export(index_exports, {
81
82
  readIdentity: () => readIdentity,
82
83
  readManifest: () => readManifest,
83
84
  readToken: () => readToken,
85
+ registerDirectViaPostgres: () => registerDirectViaPostgres,
84
86
  registerNativeEntities: () => registerNativeEntities,
85
87
  saveDbCredential: () => saveDbCredential,
86
88
  slugify: () => slugify,
@@ -5568,9 +5570,142 @@ async function attachBlob(srcPath, latticeRoot) {
5568
5570
  }
5569
5571
 
5570
5572
  // src/teams/client.ts
5571
- var import_node_crypto8 = require("crypto");
5573
+ var import_node_crypto9 = require("crypto");
5572
5574
 
5573
5575
  // src/teams/internal-tables.ts
5576
+ var CLOUD_INTERNAL_TABLE_DEFS = {
5577
+ __lattice_users: {
5578
+ columns: {
5579
+ id: "TEXT PRIMARY KEY",
5580
+ email: "TEXT NOT NULL",
5581
+ name: "TEXT",
5582
+ created_at: "TEXT NOT NULL",
5583
+ updated_at: "TEXT NOT NULL",
5584
+ deleted_at: "TEXT"
5585
+ },
5586
+ // Uniqueness enforced at the route layer (we soft-delete by setting
5587
+ // deleted_at, so a column-level UNIQUE blocks re-registration after
5588
+ // a delete). The register/redeem handlers check for an existing
5589
+ // non-deleted user with the same email before insert.
5590
+ render: () => "",
5591
+ outputFile: ".lattice-teams/users.md"
5592
+ },
5593
+ __lattice_api_tokens: {
5594
+ columns: {
5595
+ id: "TEXT PRIMARY KEY",
5596
+ user_id: "TEXT NOT NULL",
5597
+ token_hash: "TEXT NOT NULL UNIQUE",
5598
+ name: "TEXT",
5599
+ created_at: "TEXT NOT NULL",
5600
+ last_used_at: "TEXT",
5601
+ revoked_at: "TEXT"
5602
+ },
5603
+ render: () => "",
5604
+ outputFile: ".lattice-teams/tokens.md"
5605
+ },
5606
+ __lattice_team: {
5607
+ columns: {
5608
+ id: "TEXT PRIMARY KEY",
5609
+ name: "TEXT NOT NULL",
5610
+ created_by_user_id: "TEXT NOT NULL",
5611
+ created_at: "TEXT NOT NULL",
5612
+ updated_at: "TEXT NOT NULL",
5613
+ deleted_at: "TEXT"
5614
+ },
5615
+ render: () => "",
5616
+ outputFile: ".lattice-teams/teams.md"
5617
+ },
5618
+ // Singleton mirror of __lattice_team — populated by `createTeam` when
5619
+ // the first (and only) team is established on this cloud. One row per
5620
+ // DB, id='singleton'. Lets GET /api/team / DELETE /api/team / POST
5621
+ // /api/team/invitations resolve the active team without scanning.
5622
+ // The multi-team `__lattice_team` table remains the source of truth
5623
+ // for the row/object/changes routes until Step 8 deprecates them.
5624
+ __lattice_team_identity: {
5625
+ columns: {
5626
+ id: "TEXT PRIMARY KEY",
5627
+ team_id: "TEXT NOT NULL",
5628
+ team_name: "TEXT NOT NULL",
5629
+ creator_email: "TEXT NOT NULL",
5630
+ created_at: "TEXT NOT NULL"
5631
+ },
5632
+ primaryKey: "id",
5633
+ render: () => "",
5634
+ outputFile: ".lattice-teams/team-identity.md"
5635
+ },
5636
+ __lattice_team_members: {
5637
+ columns: {
5638
+ team_id: "TEXT NOT NULL",
5639
+ user_id: "TEXT NOT NULL",
5640
+ role: "TEXT NOT NULL CHECK (role IN ('creator', 'member'))",
5641
+ joined_at: "TEXT NOT NULL"
5642
+ },
5643
+ primaryKey: ["team_id", "user_id"],
5644
+ render: () => "",
5645
+ outputFile: ".lattice-teams/members.md"
5646
+ },
5647
+ __lattice_invitations: {
5648
+ columns: {
5649
+ id: "TEXT PRIMARY KEY",
5650
+ team_id: "TEXT NOT NULL",
5651
+ token_hash: "TEXT NOT NULL UNIQUE",
5652
+ // Email the invitation is addressed to — redeem requires the
5653
+ // caller's identity email to match. Email-binding makes invite
5654
+ // codes safe to share over a channel that's not strongly
5655
+ // authenticated; the recipient still has to be the recipient.
5656
+ invitee_email: "TEXT NOT NULL",
5657
+ invited_by_user_id: "TEXT NOT NULL",
5658
+ created_at: "TEXT NOT NULL",
5659
+ expires_at: "TEXT",
5660
+ redeemed_at: "TEXT",
5661
+ redeemed_by_user_id: "TEXT"
5662
+ },
5663
+ render: () => "",
5664
+ outputFile: ".lattice-teams/invitations.md"
5665
+ },
5666
+ __lattice_shared_objects: {
5667
+ columns: {
5668
+ team_id: "TEXT NOT NULL",
5669
+ table_name: "TEXT NOT NULL",
5670
+ schema_spec_json: "TEXT NOT NULL",
5671
+ schema_version: "INTEGER NOT NULL",
5672
+ created_by_user_id: "TEXT NOT NULL",
5673
+ created_at: "TEXT NOT NULL",
5674
+ updated_at: "TEXT NOT NULL",
5675
+ deleted_at: "TEXT"
5676
+ },
5677
+ primaryKey: ["team_id", "table_name"],
5678
+ render: () => "",
5679
+ outputFile: ".lattice-teams/shared-objects.md"
5680
+ },
5681
+ __lattice_change_log: {
5682
+ columns: {
5683
+ id: "TEXT PRIMARY KEY",
5684
+ seq: "INTEGER NOT NULL",
5685
+ team_id: "TEXT NOT NULL",
5686
+ table_name: "TEXT",
5687
+ pk: "TEXT",
5688
+ op: "TEXT NOT NULL",
5689
+ payload_json: "TEXT",
5690
+ owner_user_id: "TEXT",
5691
+ created_at: "TEXT NOT NULL"
5692
+ },
5693
+ render: () => "",
5694
+ outputFile: ".lattice-teams/change-log.md"
5695
+ },
5696
+ __lattice_row_links: {
5697
+ columns: {
5698
+ team_id: "TEXT NOT NULL",
5699
+ table_name: "TEXT NOT NULL",
5700
+ pk: "TEXT NOT NULL",
5701
+ owner_user_id: "TEXT NOT NULL",
5702
+ linked_at: "TEXT NOT NULL"
5703
+ },
5704
+ primaryKey: ["team_id", "table_name", "pk"],
5705
+ render: () => "",
5706
+ outputFile: ".lattice-teams/row-links.md"
5707
+ }
5708
+ };
5574
5709
  var LOCAL_INTERNAL_TABLE_DEFS = {
5575
5710
  __lattice_team_connections: {
5576
5711
  columns: {
@@ -5753,6 +5888,98 @@ async function probeCloud(targetUrl) {
5753
5888
  }
5754
5889
  }
5755
5890
 
5891
+ // src/teams/server/auth.ts
5892
+ var import_node_crypto8 = require("crypto");
5893
+ var TOKEN_PREFIX = "lat_";
5894
+ var TOKEN_BYTES = 32;
5895
+ function hashToken(rawToken) {
5896
+ return (0, import_node_crypto8.createHash)("sha256").update(rawToken).digest("hex");
5897
+ }
5898
+ function generateToken() {
5899
+ const raw = `${TOKEN_PREFIX}${(0, import_node_crypto8.randomBytes)(TOKEN_BYTES).toString("hex")}`;
5900
+ return { raw, hash: hashToken(raw) };
5901
+ }
5902
+
5903
+ // src/teams/register-direct.ts
5904
+ function isPostgresUrl(url) {
5905
+ return /^postgres(ql)?:\/\//i.test(url);
5906
+ }
5907
+ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
5908
+ if (!isPostgresUrl(cloudUrl)) {
5909
+ throw new Error(
5910
+ `registerDirectViaPostgres: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
5911
+ );
5912
+ }
5913
+ const db = new Lattice(cloudUrl);
5914
+ try {
5915
+ await db.init();
5916
+ for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
5917
+ await db.defineLate(table, def);
5918
+ }
5919
+ const existing = await db.query("__lattice_users", {
5920
+ filters: [{ col: "deleted_at", op: "isNull" }],
5921
+ limit: 1
5922
+ });
5923
+ if (existing.length > 0) {
5924
+ throw new Error(
5925
+ "Registration is disabled. This cloud already has users \u2014 join via invitation."
5926
+ );
5927
+ }
5928
+ let identity = null;
5929
+ try {
5930
+ identity = await db.get("__lattice_team_identity", "singleton");
5931
+ } catch {
5932
+ identity = null;
5933
+ }
5934
+ if (identity) {
5935
+ throw new Error("This cloud already has a team. Use Connect to existing cloud instead.");
5936
+ }
5937
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5938
+ const userId = await db.insert("__lattice_users", {
5939
+ email,
5940
+ name,
5941
+ created_at: now,
5942
+ updated_at: now
5943
+ });
5944
+ const { raw, hash } = generateToken();
5945
+ await db.insert("__lattice_api_tokens", {
5946
+ user_id: userId,
5947
+ token_hash: hash,
5948
+ name: `creator:${teamName}`,
5949
+ created_at: now
5950
+ });
5951
+ const teamId = await db.insert("__lattice_team", {
5952
+ name: teamName,
5953
+ created_by_user_id: userId,
5954
+ created_at: now,
5955
+ updated_at: now
5956
+ });
5957
+ await db.insert("__lattice_team_members", {
5958
+ team_id: teamId,
5959
+ user_id: userId,
5960
+ role: "creator",
5961
+ joined_at: now
5962
+ });
5963
+ await db.insert("__lattice_team_identity", {
5964
+ id: "singleton",
5965
+ team_id: teamId,
5966
+ team_name: teamName,
5967
+ creator_email: email,
5968
+ created_at: now
5969
+ });
5970
+ return {
5971
+ user: { id: userId, email, name },
5972
+ raw_token: raw,
5973
+ team: { id: teamId, name: teamName, role: "creator" }
5974
+ };
5975
+ } finally {
5976
+ try {
5977
+ db.close();
5978
+ } catch {
5979
+ }
5980
+ }
5981
+ }
5982
+
5756
5983
  // src/teams/client.ts
5757
5984
  var TeamsClient = class {
5758
5985
  constructor(local) {
@@ -5848,15 +6075,22 @@ var TeamsClient = class {
5848
6075
  return { probe };
5849
6076
  }
5850
6077
  /**
5851
- * Upgrade an already-connected cloud DB to a team DB. Runs the
5852
- * atomic `POST /api/auth/register` flow against the cloud URL
5853
- * stored under `label`. Writes the resulting bearer to
5854
- * `~/.lattice/keys/<label>.token`. Caller is expected to call
5855
- * `saveConnection` if they also want the local
5856
- * `__lattice_team_connections` row populated.
6078
+ * Upgrade an already-connected cloud DB to a team DB. Two paths
6079
+ * depending on the cloud URL's scheme:
6080
+ *
6081
+ * - `http(s)://…` POST to the cloud's `/api/auth/register` endpoint
6082
+ * (`lattice serve --team-cloud` is fronting the Postgres).
6083
+ * - `postgres(ql)://…` drive the same INSERT sequence directly
6084
+ * against the cloud Postgres via {@link registerDirectViaPostgres}.
6085
+ * The HTTP path can't be used here because the browser's Fetch
6086
+ * API refuses URLs with embedded credentials.
6087
+ *
6088
+ * On success writes the bearer token to `~/.lattice/keys/<label>.token`.
6089
+ * Caller is expected to call `saveConnection` if they also want the
6090
+ * local `__lattice_team_connections` row populated.
5857
6091
  */
5858
6092
  async upgradeToTeamCloud(opts) {
5859
- const reg = await this.register(opts.cloudUrl, opts.email, opts.displayName, opts.teamName);
6093
+ const reg = isPostgresUrl(opts.cloudUrl) ? await registerDirectViaPostgres(opts.cloudUrl, opts.email, opts.displayName, opts.teamName) : await this.register(opts.cloudUrl, opts.email, opts.displayName, opts.teamName);
5860
6094
  writeToken(opts.label, reg.raw_token);
5861
6095
  return reg;
5862
6096
  }
@@ -6161,7 +6395,7 @@ var TeamsClient = class {
6161
6395
  if (conn.my_user_id !== link.owner_user_id) continue;
6162
6396
  const now = (/* @__PURE__ */ new Date()).toISOString();
6163
6397
  await this.local.insert("__lattice_team_outbox", {
6164
- id: (0, import_node_crypto8.randomUUID)(),
6398
+ id: (0, import_node_crypto9.randomUUID)(),
6165
6399
  team_id: link.team_id,
6166
6400
  table_name: ctx.table,
6167
6401
  pk: ctx.pk,
@@ -6270,7 +6504,7 @@ var TeamsClient = class {
6270
6504
  totalApplied++;
6271
6505
  } catch (e) {
6272
6506
  await this.local.insert("__lattice_team_dlq", {
6273
- id: (0, import_node_crypto8.randomUUID)(),
6507
+ id: (0, import_node_crypto9.randomUUID)(),
6274
6508
  team_id: connection.team_id,
6275
6509
  envelope_json: JSON.stringify(env),
6276
6510
  error: e.message,
@@ -6524,6 +6758,7 @@ function archiveLocalSqlite(dbPath) {
6524
6758
  getOrCreateMasterKey,
6525
6759
  hashFile,
6526
6760
  isEncrypted,
6761
+ isPostgresUrl,
6527
6762
  isV1EntityFiles,
6528
6763
  listDbCredentials,
6529
6764
  listTokens,
@@ -6541,6 +6776,7 @@ function archiveLocalSqlite(dbPath) {
6541
6776
  readIdentity,
6542
6777
  readManifest,
6543
6778
  readToken,
6779
+ registerDirectViaPostgres,
6544
6780
  registerNativeEntities,
6545
6781
  saveDbCredential,
6546
6782
  slugify,
package/dist/index.d.cts CHANGED
@@ -3017,12 +3017,19 @@ declare class TeamsClient {
3017
3017
  };
3018
3018
  }>;
3019
3019
  /**
3020
- * Upgrade an already-connected cloud DB to a team DB. Runs the
3021
- * atomic `POST /api/auth/register` flow against the cloud URL
3022
- * stored under `label`. Writes the resulting bearer to
3023
- * `~/.lattice/keys/<label>.token`. Caller is expected to call
3024
- * `saveConnection` if they also want the local
3025
- * `__lattice_team_connections` row populated.
3020
+ * Upgrade an already-connected cloud DB to a team DB. Two paths
3021
+ * depending on the cloud URL's scheme:
3022
+ *
3023
+ * - `http(s)://…` POST to the cloud's `/api/auth/register` endpoint
3024
+ * (`lattice serve --team-cloud` is fronting the Postgres).
3025
+ * - `postgres(ql)://…` drive the same INSERT sequence directly
3026
+ * against the cloud Postgres via {@link registerDirectViaPostgres}.
3027
+ * The HTTP path can't be used here because the browser's Fetch
3028
+ * API refuses URLs with embedded credentials.
3029
+ *
3030
+ * On success writes the bearer token to `~/.lattice/keys/<label>.token`.
3031
+ * Caller is expected to call `saveConnection` if they also want the
3032
+ * local `__lattice_team_connections` row populated.
3026
3033
  */
3027
3034
  upgradeToTeamCloud(opts: {
3028
3035
  label: string;
@@ -3263,4 +3270,54 @@ declare function openTargetLatticeForMigration(configPath: string, targetUrl: st
3263
3270
  */
3264
3271
  declare function archiveLocalSqlite(dbPath: string): string;
3265
3272
 
3266
- export { type ApplyWriteResult, type AuditEvent, type AutoUpdateResult, type BelongsToRelation, type BelongsToSource, type BlobMetadata, type BuiltinTemplateName, type ChangeEntry, type ChangelogOptions, type CleanupOptions, type CleanupResult, type CloudProbeResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EmbeddingsConfig, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileManifestInfo, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, InMemoryStateStore, type InitOptions, type InviteResponse, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type LinkOptions, type ManyToManySource, type MarkdownTableColumn, type MemberSummary, type Migration, type MigrationOptions, type MigrationProgress, type MigrationResult, type MultiTableDefinition, NATIVE_ENTITY_DEFS, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, PostgresAdapter, type PostgresAdapterOptions, type PreparedStatement, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type RedeemResponse, type RegisterResponse, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type ReportConfig, type ReportResult, type ReportSection, type ReportSectionResult, type ReverseSeedDetection, type ReverseSeedResult, type ReverseSeedTableResult, type ReverseSyncError, type ReverseSyncResult, type ReverseSyncUpdate, type RewardScores, type Row, SQLiteAdapter, type SearchOptions, type SearchResult, type SecurityOptions, type SeedConfig, type SeedLinkSpec, type SeedResult, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type StorageAdapter, type SyncResult, type TableDefinition, type TeamConnection, type TeamSummary, TeamsClient, TeamsHttpError, type TemplateRenderSpec, type UpsertByNaturalKeyOptions, type UserIdentity, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, type WritebackStateStore, type WritebackValidationResult, applyTokenBudget, applyWriteEntry, archiveLocalSqlite, attachBlob, autoUpdate, configDir, contentHash, createReadOnlyHeader, createSQLiteStateStore, decrypt, deleteDbCredential, deleteToken, deriveKey, encrypt, entityFileNames, estimateTokens, fixSchemaConflicts, frontmatter, generateEntryId, generateWriteEntryId, getDbCredential, getOrCreateMasterKey, hashFile, isEncrypted, isV1EntityFiles, listDbCredentials, listTokens, manifestPath, markdownTable, migrateLatticeData, normalizeEntityFiles, openTargetLatticeForMigration, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, probeCloud, readIdentity, readManifest, readToken, registerNativeEntities, saveDbCredential, slugify, truncate, validateEntryId, writeIdentity, writeManifest, writeToken };
3273
+ /**
3274
+ * Shape returned by both the HTTP register path and the Postgres-direct
3275
+ * register path. Kept aligned with `RegisterResponse` in
3276
+ * `src/teams/client.ts`.
3277
+ */
3278
+ interface DirectRegisterResult {
3279
+ user: {
3280
+ id: string;
3281
+ email: string;
3282
+ name: string;
3283
+ };
3284
+ raw_token: string;
3285
+ team: {
3286
+ id: string;
3287
+ name: string;
3288
+ role: 'creator';
3289
+ };
3290
+ }
3291
+ /**
3292
+ * True iff `url` parses as a `postgres://` / `postgresql://` URL — used
3293
+ * by the GUI's upgrade flow to decide between HTTP `register` and the
3294
+ * direct-Postgres path implemented here.
3295
+ */
3296
+ declare function isPostgresUrl(url: string): boolean;
3297
+ /**
3298
+ * Direct-Postgres equivalent of the cloud's `POST /api/auth/register`.
3299
+ *
3300
+ * The HTTP teams-cloud server (`lattice serve --team-cloud`) handles
3301
+ * `register` by running an INSERT sequence inside its own request
3302
+ * handler. This helper does the same sequence locally by opening the
3303
+ * cloud Postgres directly — useful when the GUI's "Migrate to cloud"
3304
+ * or "Connect to existing cloud" flow has saved the **Postgres URL**
3305
+ * as the cloud credential (no HTTP teams server in front).
3306
+ *
3307
+ * Hard browser limitation that motivates this: when `cloudUrl` is a
3308
+ * Postgres URL with embedded credentials, the HTTP path's `fetch(url)`
3309
+ * throws "Request cannot be constructed from a URL that includes
3310
+ * credentials" before any network IO happens. We have to drive the
3311
+ * register flow against the database protocol, not HTTP.
3312
+ *
3313
+ * Mirrors `handleRegister`'s invariants:
3314
+ * - Refuses if any non-deleted user already exists on the cloud.
3315
+ * - Refuses if the `__lattice_team_identity` singleton already exists.
3316
+ *
3317
+ * On success returns the same shape the HTTP route returns so the
3318
+ * caller (`TeamsClient.upgradeToTeamCloud`) can use either path
3319
+ * interchangeably.
3320
+ */
3321
+ declare function registerDirectViaPostgres(cloudUrl: string, email: string, name: string, teamName: string): Promise<DirectRegisterResult>;
3322
+
3323
+ export { type ApplyWriteResult, type AuditEvent, type AutoUpdateResult, type BelongsToRelation, type BelongsToSource, type BlobMetadata, type BuiltinTemplateName, type ChangeEntry, type ChangelogOptions, type CleanupOptions, type CleanupResult, type CloudProbeResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type DirectRegisterResult, type EmbeddingsConfig, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileManifestInfo, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, InMemoryStateStore, type InitOptions, type InviteResponse, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type LinkOptions, type ManyToManySource, type MarkdownTableColumn, type MemberSummary, type Migration, type MigrationOptions, type MigrationProgress, type MigrationResult, type MultiTableDefinition, NATIVE_ENTITY_DEFS, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, PostgresAdapter, type PostgresAdapterOptions, type PreparedStatement, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type RedeemResponse, type RegisterResponse, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type ReportConfig, type ReportResult, type ReportSection, type ReportSectionResult, type ReverseSeedDetection, type ReverseSeedResult, type ReverseSeedTableResult, type ReverseSyncError, type ReverseSyncResult, type ReverseSyncUpdate, type RewardScores, type Row, SQLiteAdapter, type SearchOptions, type SearchResult, type SecurityOptions, type SeedConfig, type SeedLinkSpec, type SeedResult, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type StorageAdapter, type SyncResult, type TableDefinition, type TeamConnection, type TeamSummary, TeamsClient, TeamsHttpError, type TemplateRenderSpec, type UpsertByNaturalKeyOptions, type UserIdentity, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, type WritebackStateStore, type WritebackValidationResult, applyTokenBudget, applyWriteEntry, archiveLocalSqlite, attachBlob, autoUpdate, configDir, contentHash, createReadOnlyHeader, createSQLiteStateStore, decrypt, deleteDbCredential, deleteToken, deriveKey, encrypt, entityFileNames, estimateTokens, fixSchemaConflicts, frontmatter, generateEntryId, generateWriteEntryId, getDbCredential, getOrCreateMasterKey, hashFile, isEncrypted, isPostgresUrl, isV1EntityFiles, listDbCredentials, listTokens, manifestPath, markdownTable, migrateLatticeData, normalizeEntityFiles, openTargetLatticeForMigration, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, probeCloud, readIdentity, readManifest, readToken, registerDirectViaPostgres, registerNativeEntities, saveDbCredential, slugify, truncate, validateEntryId, writeIdentity, writeManifest, writeToken };
package/dist/index.d.ts CHANGED
@@ -3017,12 +3017,19 @@ declare class TeamsClient {
3017
3017
  };
3018
3018
  }>;
3019
3019
  /**
3020
- * Upgrade an already-connected cloud DB to a team DB. Runs the
3021
- * atomic `POST /api/auth/register` flow against the cloud URL
3022
- * stored under `label`. Writes the resulting bearer to
3023
- * `~/.lattice/keys/<label>.token`. Caller is expected to call
3024
- * `saveConnection` if they also want the local
3025
- * `__lattice_team_connections` row populated.
3020
+ * Upgrade an already-connected cloud DB to a team DB. Two paths
3021
+ * depending on the cloud URL's scheme:
3022
+ *
3023
+ * - `http(s)://…` POST to the cloud's `/api/auth/register` endpoint
3024
+ * (`lattice serve --team-cloud` is fronting the Postgres).
3025
+ * - `postgres(ql)://…` drive the same INSERT sequence directly
3026
+ * against the cloud Postgres via {@link registerDirectViaPostgres}.
3027
+ * The HTTP path can't be used here because the browser's Fetch
3028
+ * API refuses URLs with embedded credentials.
3029
+ *
3030
+ * On success writes the bearer token to `~/.lattice/keys/<label>.token`.
3031
+ * Caller is expected to call `saveConnection` if they also want the
3032
+ * local `__lattice_team_connections` row populated.
3026
3033
  */
3027
3034
  upgradeToTeamCloud(opts: {
3028
3035
  label: string;
@@ -3263,4 +3270,54 @@ declare function openTargetLatticeForMigration(configPath: string, targetUrl: st
3263
3270
  */
3264
3271
  declare function archiveLocalSqlite(dbPath: string): string;
3265
3272
 
3266
- export { type ApplyWriteResult, type AuditEvent, type AutoUpdateResult, type BelongsToRelation, type BelongsToSource, type BlobMetadata, type BuiltinTemplateName, type ChangeEntry, type ChangelogOptions, type CleanupOptions, type CleanupResult, type CloudProbeResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EmbeddingsConfig, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileManifestInfo, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, InMemoryStateStore, type InitOptions, type InviteResponse, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type LinkOptions, type ManyToManySource, type MarkdownTableColumn, type MemberSummary, type Migration, type MigrationOptions, type MigrationProgress, type MigrationResult, type MultiTableDefinition, NATIVE_ENTITY_DEFS, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, PostgresAdapter, type PostgresAdapterOptions, type PreparedStatement, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type RedeemResponse, type RegisterResponse, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type ReportConfig, type ReportResult, type ReportSection, type ReportSectionResult, type ReverseSeedDetection, type ReverseSeedResult, type ReverseSeedTableResult, type ReverseSyncError, type ReverseSyncResult, type ReverseSyncUpdate, type RewardScores, type Row, SQLiteAdapter, type SearchOptions, type SearchResult, type SecurityOptions, type SeedConfig, type SeedLinkSpec, type SeedResult, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type StorageAdapter, type SyncResult, type TableDefinition, type TeamConnection, type TeamSummary, TeamsClient, TeamsHttpError, type TemplateRenderSpec, type UpsertByNaturalKeyOptions, type UserIdentity, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, type WritebackStateStore, type WritebackValidationResult, applyTokenBudget, applyWriteEntry, archiveLocalSqlite, attachBlob, autoUpdate, configDir, contentHash, createReadOnlyHeader, createSQLiteStateStore, decrypt, deleteDbCredential, deleteToken, deriveKey, encrypt, entityFileNames, estimateTokens, fixSchemaConflicts, frontmatter, generateEntryId, generateWriteEntryId, getDbCredential, getOrCreateMasterKey, hashFile, isEncrypted, isV1EntityFiles, listDbCredentials, listTokens, manifestPath, markdownTable, migrateLatticeData, normalizeEntityFiles, openTargetLatticeForMigration, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, probeCloud, readIdentity, readManifest, readToken, registerNativeEntities, saveDbCredential, slugify, truncate, validateEntryId, writeIdentity, writeManifest, writeToken };
3273
+ /**
3274
+ * Shape returned by both the HTTP register path and the Postgres-direct
3275
+ * register path. Kept aligned with `RegisterResponse` in
3276
+ * `src/teams/client.ts`.
3277
+ */
3278
+ interface DirectRegisterResult {
3279
+ user: {
3280
+ id: string;
3281
+ email: string;
3282
+ name: string;
3283
+ };
3284
+ raw_token: string;
3285
+ team: {
3286
+ id: string;
3287
+ name: string;
3288
+ role: 'creator';
3289
+ };
3290
+ }
3291
+ /**
3292
+ * True iff `url` parses as a `postgres://` / `postgresql://` URL — used
3293
+ * by the GUI's upgrade flow to decide between HTTP `register` and the
3294
+ * direct-Postgres path implemented here.
3295
+ */
3296
+ declare function isPostgresUrl(url: string): boolean;
3297
+ /**
3298
+ * Direct-Postgres equivalent of the cloud's `POST /api/auth/register`.
3299
+ *
3300
+ * The HTTP teams-cloud server (`lattice serve --team-cloud`) handles
3301
+ * `register` by running an INSERT sequence inside its own request
3302
+ * handler. This helper does the same sequence locally by opening the
3303
+ * cloud Postgres directly — useful when the GUI's "Migrate to cloud"
3304
+ * or "Connect to existing cloud" flow has saved the **Postgres URL**
3305
+ * as the cloud credential (no HTTP teams server in front).
3306
+ *
3307
+ * Hard browser limitation that motivates this: when `cloudUrl` is a
3308
+ * Postgres URL with embedded credentials, the HTTP path's `fetch(url)`
3309
+ * throws "Request cannot be constructed from a URL that includes
3310
+ * credentials" before any network IO happens. We have to drive the
3311
+ * register flow against the database protocol, not HTTP.
3312
+ *
3313
+ * Mirrors `handleRegister`'s invariants:
3314
+ * - Refuses if any non-deleted user already exists on the cloud.
3315
+ * - Refuses if the `__lattice_team_identity` singleton already exists.
3316
+ *
3317
+ * On success returns the same shape the HTTP route returns so the
3318
+ * caller (`TeamsClient.upgradeToTeamCloud`) can use either path
3319
+ * interchangeably.
3320
+ */
3321
+ declare function registerDirectViaPostgres(cloudUrl: string, email: string, name: string, teamName: string): Promise<DirectRegisterResult>;
3322
+
3323
+ export { type ApplyWriteResult, type AuditEvent, type AutoUpdateResult, type BelongsToRelation, type BelongsToSource, type BlobMetadata, type BuiltinTemplateName, type ChangeEntry, type ChangelogOptions, type CleanupOptions, type CleanupResult, type CloudProbeResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type DirectRegisterResult, type EmbeddingsConfig, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileManifestInfo, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, InMemoryStateStore, type InitOptions, type InviteResponse, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type LinkOptions, type ManyToManySource, type MarkdownTableColumn, type MemberSummary, type Migration, type MigrationOptions, type MigrationProgress, type MigrationResult, type MultiTableDefinition, NATIVE_ENTITY_DEFS, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, PostgresAdapter, type PostgresAdapterOptions, type PreparedStatement, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type RedeemResponse, type RegisterResponse, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type ReportConfig, type ReportResult, type ReportSection, type ReportSectionResult, type ReverseSeedDetection, type ReverseSeedResult, type ReverseSeedTableResult, type ReverseSyncError, type ReverseSyncResult, type ReverseSyncUpdate, type RewardScores, type Row, SQLiteAdapter, type SearchOptions, type SearchResult, type SecurityOptions, type SeedConfig, type SeedLinkSpec, type SeedResult, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type StorageAdapter, type SyncResult, type TableDefinition, type TeamConnection, type TeamSummary, TeamsClient, TeamsHttpError, type TemplateRenderSpec, type UpsertByNaturalKeyOptions, type UserIdentity, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, type WritebackStateStore, type WritebackValidationResult, applyTokenBudget, applyWriteEntry, archiveLocalSqlite, attachBlob, autoUpdate, configDir, contentHash, createReadOnlyHeader, createSQLiteStateStore, decrypt, deleteDbCredential, deleteToken, deriveKey, encrypt, entityFileNames, estimateTokens, fixSchemaConflicts, frontmatter, generateEntryId, generateWriteEntryId, getDbCredential, getOrCreateMasterKey, hashFile, isEncrypted, isPostgresUrl, isV1EntityFiles, listDbCredentials, listTokens, manifestPath, markdownTable, migrateLatticeData, normalizeEntityFiles, openTargetLatticeForMigration, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, probeCloud, readIdentity, readManifest, readToken, registerDirectViaPostgres, registerNativeEntities, saveDbCredential, slugify, truncate, validateEntryId, writeIdentity, writeManifest, writeToken };
package/dist/index.js CHANGED
@@ -5499,6 +5499,139 @@ async function attachBlob(srcPath, latticeRoot) {
5499
5499
  import { randomUUID } from "crypto";
5500
5500
 
5501
5501
  // src/teams/internal-tables.ts
5502
+ var CLOUD_INTERNAL_TABLE_DEFS = {
5503
+ __lattice_users: {
5504
+ columns: {
5505
+ id: "TEXT PRIMARY KEY",
5506
+ email: "TEXT NOT NULL",
5507
+ name: "TEXT",
5508
+ created_at: "TEXT NOT NULL",
5509
+ updated_at: "TEXT NOT NULL",
5510
+ deleted_at: "TEXT"
5511
+ },
5512
+ // Uniqueness enforced at the route layer (we soft-delete by setting
5513
+ // deleted_at, so a column-level UNIQUE blocks re-registration after
5514
+ // a delete). The register/redeem handlers check for an existing
5515
+ // non-deleted user with the same email before insert.
5516
+ render: () => "",
5517
+ outputFile: ".lattice-teams/users.md"
5518
+ },
5519
+ __lattice_api_tokens: {
5520
+ columns: {
5521
+ id: "TEXT PRIMARY KEY",
5522
+ user_id: "TEXT NOT NULL",
5523
+ token_hash: "TEXT NOT NULL UNIQUE",
5524
+ name: "TEXT",
5525
+ created_at: "TEXT NOT NULL",
5526
+ last_used_at: "TEXT",
5527
+ revoked_at: "TEXT"
5528
+ },
5529
+ render: () => "",
5530
+ outputFile: ".lattice-teams/tokens.md"
5531
+ },
5532
+ __lattice_team: {
5533
+ columns: {
5534
+ id: "TEXT PRIMARY KEY",
5535
+ name: "TEXT NOT NULL",
5536
+ created_by_user_id: "TEXT NOT NULL",
5537
+ created_at: "TEXT NOT NULL",
5538
+ updated_at: "TEXT NOT NULL",
5539
+ deleted_at: "TEXT"
5540
+ },
5541
+ render: () => "",
5542
+ outputFile: ".lattice-teams/teams.md"
5543
+ },
5544
+ // Singleton mirror of __lattice_team — populated by `createTeam` when
5545
+ // the first (and only) team is established on this cloud. One row per
5546
+ // DB, id='singleton'. Lets GET /api/team / DELETE /api/team / POST
5547
+ // /api/team/invitations resolve the active team without scanning.
5548
+ // The multi-team `__lattice_team` table remains the source of truth
5549
+ // for the row/object/changes routes until Step 8 deprecates them.
5550
+ __lattice_team_identity: {
5551
+ columns: {
5552
+ id: "TEXT PRIMARY KEY",
5553
+ team_id: "TEXT NOT NULL",
5554
+ team_name: "TEXT NOT NULL",
5555
+ creator_email: "TEXT NOT NULL",
5556
+ created_at: "TEXT NOT NULL"
5557
+ },
5558
+ primaryKey: "id",
5559
+ render: () => "",
5560
+ outputFile: ".lattice-teams/team-identity.md"
5561
+ },
5562
+ __lattice_team_members: {
5563
+ columns: {
5564
+ team_id: "TEXT NOT NULL",
5565
+ user_id: "TEXT NOT NULL",
5566
+ role: "TEXT NOT NULL CHECK (role IN ('creator', 'member'))",
5567
+ joined_at: "TEXT NOT NULL"
5568
+ },
5569
+ primaryKey: ["team_id", "user_id"],
5570
+ render: () => "",
5571
+ outputFile: ".lattice-teams/members.md"
5572
+ },
5573
+ __lattice_invitations: {
5574
+ columns: {
5575
+ id: "TEXT PRIMARY KEY",
5576
+ team_id: "TEXT NOT NULL",
5577
+ token_hash: "TEXT NOT NULL UNIQUE",
5578
+ // Email the invitation is addressed to — redeem requires the
5579
+ // caller's identity email to match. Email-binding makes invite
5580
+ // codes safe to share over a channel that's not strongly
5581
+ // authenticated; the recipient still has to be the recipient.
5582
+ invitee_email: "TEXT NOT NULL",
5583
+ invited_by_user_id: "TEXT NOT NULL",
5584
+ created_at: "TEXT NOT NULL",
5585
+ expires_at: "TEXT",
5586
+ redeemed_at: "TEXT",
5587
+ redeemed_by_user_id: "TEXT"
5588
+ },
5589
+ render: () => "",
5590
+ outputFile: ".lattice-teams/invitations.md"
5591
+ },
5592
+ __lattice_shared_objects: {
5593
+ columns: {
5594
+ team_id: "TEXT NOT NULL",
5595
+ table_name: "TEXT NOT NULL",
5596
+ schema_spec_json: "TEXT NOT NULL",
5597
+ schema_version: "INTEGER NOT NULL",
5598
+ created_by_user_id: "TEXT NOT NULL",
5599
+ created_at: "TEXT NOT NULL",
5600
+ updated_at: "TEXT NOT NULL",
5601
+ deleted_at: "TEXT"
5602
+ },
5603
+ primaryKey: ["team_id", "table_name"],
5604
+ render: () => "",
5605
+ outputFile: ".lattice-teams/shared-objects.md"
5606
+ },
5607
+ __lattice_change_log: {
5608
+ columns: {
5609
+ id: "TEXT PRIMARY KEY",
5610
+ seq: "INTEGER NOT NULL",
5611
+ team_id: "TEXT NOT NULL",
5612
+ table_name: "TEXT",
5613
+ pk: "TEXT",
5614
+ op: "TEXT NOT NULL",
5615
+ payload_json: "TEXT",
5616
+ owner_user_id: "TEXT",
5617
+ created_at: "TEXT NOT NULL"
5618
+ },
5619
+ render: () => "",
5620
+ outputFile: ".lattice-teams/change-log.md"
5621
+ },
5622
+ __lattice_row_links: {
5623
+ columns: {
5624
+ team_id: "TEXT NOT NULL",
5625
+ table_name: "TEXT NOT NULL",
5626
+ pk: "TEXT NOT NULL",
5627
+ owner_user_id: "TEXT NOT NULL",
5628
+ linked_at: "TEXT NOT NULL"
5629
+ },
5630
+ primaryKey: ["team_id", "table_name", "pk"],
5631
+ render: () => "",
5632
+ outputFile: ".lattice-teams/row-links.md"
5633
+ }
5634
+ };
5502
5635
  var LOCAL_INTERNAL_TABLE_DEFS = {
5503
5636
  __lattice_team_connections: {
5504
5637
  columns: {
@@ -5681,6 +5814,98 @@ async function probeCloud(targetUrl) {
5681
5814
  }
5682
5815
  }
5683
5816
 
5817
+ // src/teams/server/auth.ts
5818
+ import { createHash as createHash5, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
5819
+ var TOKEN_PREFIX = "lat_";
5820
+ var TOKEN_BYTES = 32;
5821
+ function hashToken(rawToken) {
5822
+ return createHash5("sha256").update(rawToken).digest("hex");
5823
+ }
5824
+ function generateToken() {
5825
+ const raw = `${TOKEN_PREFIX}${randomBytes4(TOKEN_BYTES).toString("hex")}`;
5826
+ return { raw, hash: hashToken(raw) };
5827
+ }
5828
+
5829
+ // src/teams/register-direct.ts
5830
+ function isPostgresUrl(url) {
5831
+ return /^postgres(ql)?:\/\//i.test(url);
5832
+ }
5833
+ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
5834
+ if (!isPostgresUrl(cloudUrl)) {
5835
+ throw new Error(
5836
+ `registerDirectViaPostgres: cloudUrl must be a postgres:// URL (got ${cloudUrl.slice(0, 12)}\u2026)`
5837
+ );
5838
+ }
5839
+ const db = new Lattice(cloudUrl);
5840
+ try {
5841
+ await db.init();
5842
+ for (const [table, def] of Object.entries(CLOUD_INTERNAL_TABLE_DEFS)) {
5843
+ await db.defineLate(table, def);
5844
+ }
5845
+ const existing = await db.query("__lattice_users", {
5846
+ filters: [{ col: "deleted_at", op: "isNull" }],
5847
+ limit: 1
5848
+ });
5849
+ if (existing.length > 0) {
5850
+ throw new Error(
5851
+ "Registration is disabled. This cloud already has users \u2014 join via invitation."
5852
+ );
5853
+ }
5854
+ let identity = null;
5855
+ try {
5856
+ identity = await db.get("__lattice_team_identity", "singleton");
5857
+ } catch {
5858
+ identity = null;
5859
+ }
5860
+ if (identity) {
5861
+ throw new Error("This cloud already has a team. Use Connect to existing cloud instead.");
5862
+ }
5863
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5864
+ const userId = await db.insert("__lattice_users", {
5865
+ email,
5866
+ name,
5867
+ created_at: now,
5868
+ updated_at: now
5869
+ });
5870
+ const { raw, hash } = generateToken();
5871
+ await db.insert("__lattice_api_tokens", {
5872
+ user_id: userId,
5873
+ token_hash: hash,
5874
+ name: `creator:${teamName}`,
5875
+ created_at: now
5876
+ });
5877
+ const teamId = await db.insert("__lattice_team", {
5878
+ name: teamName,
5879
+ created_by_user_id: userId,
5880
+ created_at: now,
5881
+ updated_at: now
5882
+ });
5883
+ await db.insert("__lattice_team_members", {
5884
+ team_id: teamId,
5885
+ user_id: userId,
5886
+ role: "creator",
5887
+ joined_at: now
5888
+ });
5889
+ await db.insert("__lattice_team_identity", {
5890
+ id: "singleton",
5891
+ team_id: teamId,
5892
+ team_name: teamName,
5893
+ creator_email: email,
5894
+ created_at: now
5895
+ });
5896
+ return {
5897
+ user: { id: userId, email, name },
5898
+ raw_token: raw,
5899
+ team: { id: teamId, name: teamName, role: "creator" }
5900
+ };
5901
+ } finally {
5902
+ try {
5903
+ db.close();
5904
+ } catch {
5905
+ }
5906
+ }
5907
+ }
5908
+
5684
5909
  // src/teams/client.ts
5685
5910
  var TeamsClient = class {
5686
5911
  constructor(local) {
@@ -5776,15 +6001,22 @@ var TeamsClient = class {
5776
6001
  return { probe };
5777
6002
  }
5778
6003
  /**
5779
- * Upgrade an already-connected cloud DB to a team DB. Runs the
5780
- * atomic `POST /api/auth/register` flow against the cloud URL
5781
- * stored under `label`. Writes the resulting bearer to
5782
- * `~/.lattice/keys/<label>.token`. Caller is expected to call
5783
- * `saveConnection` if they also want the local
5784
- * `__lattice_team_connections` row populated.
6004
+ * Upgrade an already-connected cloud DB to a team DB. Two paths
6005
+ * depending on the cloud URL's scheme:
6006
+ *
6007
+ * - `http(s)://…` POST to the cloud's `/api/auth/register` endpoint
6008
+ * (`lattice serve --team-cloud` is fronting the Postgres).
6009
+ * - `postgres(ql)://…` drive the same INSERT sequence directly
6010
+ * against the cloud Postgres via {@link registerDirectViaPostgres}.
6011
+ * The HTTP path can't be used here because the browser's Fetch
6012
+ * API refuses URLs with embedded credentials.
6013
+ *
6014
+ * On success writes the bearer token to `~/.lattice/keys/<label>.token`.
6015
+ * Caller is expected to call `saveConnection` if they also want the
6016
+ * local `__lattice_team_connections` row populated.
5785
6017
  */
5786
6018
  async upgradeToTeamCloud(opts) {
5787
- const reg = await this.register(opts.cloudUrl, opts.email, opts.displayName, opts.teamName);
6019
+ const reg = isPostgresUrl(opts.cloudUrl) ? await registerDirectViaPostgres(opts.cloudUrl, opts.email, opts.displayName, opts.teamName) : await this.register(opts.cloudUrl, opts.email, opts.displayName, opts.teamName);
5788
6020
  writeToken(opts.label, reg.raw_token);
5789
6021
  return reg;
5790
6022
  }
@@ -6451,6 +6683,7 @@ export {
6451
6683
  getOrCreateMasterKey,
6452
6684
  hashFile,
6453
6685
  isEncrypted,
6686
+ isPostgresUrl,
6454
6687
  isV1EntityFiles,
6455
6688
  listDbCredentials,
6456
6689
  listTokens,
@@ -6468,6 +6701,7 @@ export {
6468
6701
  readIdentity,
6469
6702
  readManifest,
6470
6703
  readToken,
6704
+ registerDirectViaPostgres,
6471
6705
  registerNativeEntities,
6472
6706
  saveDbCredential,
6473
6707
  slugify,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "1.13.2",
3
+ "version": "1.13.3",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",