latticesql 1.13.1 → 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 +8 -0
- package/dist/cli.js +299 -46
- package/dist/index.cjs +254 -11
- package/dist/index.d.cts +64 -7
- package/dist/index.d.ts +64 -7
- package/dist/index.js +249 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2082,6 +2082,14 @@ npx lattice gui --config ./lattice.config.yml --output ./context --port 4317
|
|
|
2082
2082
|
|
|
2083
2083
|
The convergence means you don't need to duplicate entity-context definitions in YAML for the GUI to find rendered files.
|
|
2084
2084
|
|
|
2085
|
+
**Database wizard form (v1.13.2+).** The Postgres connection form (used by Migrate to cloud + Connect to existing cloud) disables browser autocapitalize, autocorrect, and spellcheck on every text input, and trims whitespace on every read. This avoids silent failure modes where macOS Safari / iOS turned a Supabase tenant user `postgres.<ref>` into `Postgres.<ref>` on submit, and where pasted credentials carrying a trailing newline produced opaque "zero-length delimiter identifier" or SCRAM-mismatch errors. `probeCloud` also folds SQLSTATE + `routine` into `result.error` so the GUI's "Unreachable: …" surface is actionable.
|
|
2086
|
+
|
|
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
|
+
|
|
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
|
+
|
|
2085
2093
|
**Views**
|
|
2086
2094
|
|
|
2087
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
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
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>' +
|
|
@@ -7999,30 +8033,117 @@ var guiAppHtml = `<!doctype html>
|
|
|
7999
8033
|
|
|
8000
8034
|
function postgresFormHtml(prefill) {
|
|
8001
8035
|
prefill = prefill || {};
|
|
8036
|
+
// autocapitalize="off" + autocorrect="off" + spellcheck="false" keep
|
|
8037
|
+
// mobile / macOS keyboards from "helpfully" capitalizing the first
|
|
8038
|
+
// letter of usernames + host fragments. Supabase tenant users
|
|
8039
|
+
// (postgres.<ref>) are case-sensitive and silently failed
|
|
8040
|
+
// authentication when iOS Safari turned the leading "p" into "P".
|
|
8041
|
+
var attrs = ' autocapitalize="off" autocorrect="off" spellcheck="false"';
|
|
8002
8042
|
return (
|
|
8003
8043
|
'<div class="grid" style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">' +
|
|
8004
|
-
'<div><label class="field-label">Label</label><input type="text" id="w-label" placeholder="atlas" value="' + escapeHtml(prefill.label || '') + '" style="width:100%"></div>' +
|
|
8005
|
-
'<div><label class="field-label">Host</label><input type="text" id="w-host" placeholder="db.example.com" value="' + escapeHtml(prefill.host || '') + '" style="width:100%"></div>' +
|
|
8044
|
+
'<div><label class="field-label">Label</label><input type="text" id="w-label" placeholder="atlas" value="' + escapeHtml(prefill.label || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
8045
|
+
'<div><label class="field-label">Host</label><input type="text" id="w-host" placeholder="db.example.com" value="' + escapeHtml(prefill.host || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
8006
8046
|
'<div><label class="field-label">Port</label><input type="number" id="w-port" placeholder="5432" value="' + escapeHtml(String(prefill.port || 5432)) + '" style="width:100%"></div>' +
|
|
8007
|
-
'<div><label class="field-label">Database name</label><input type="text" id="w-dbname" placeholder="app" value="' + escapeHtml(prefill.dbname || '') + '" style="width:100%"></div>' +
|
|
8008
|
-
'<div><label class="field-label">User</label><input type="text" id="w-user" placeholder="lattice_user" value="' + escapeHtml(prefill.user || '') + '" style="width:100%"></div>' +
|
|
8009
|
-
'<div><label class="field-label">Password</label><input type="password" id="w-password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" style="width:100%"></div>' +
|
|
8047
|
+
'<div><label class="field-label">Database name</label><input type="text" id="w-dbname" placeholder="app" value="' + escapeHtml(prefill.dbname || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
8048
|
+
'<div><label class="field-label">User</label><input type="text" id="w-user" placeholder="lattice_user" value="' + escapeHtml(prefill.user || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
8049
|
+
'<div><label class="field-label">Password</label><input type="password" id="w-password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" style="width:100%"' + attrs + '></div>' +
|
|
8010
8050
|
'</div>'
|
|
8011
8051
|
);
|
|
8012
8052
|
}
|
|
8013
8053
|
|
|
8014
8054
|
function readPostgresWizardForm() {
|
|
8055
|
+
// Every text field is trimmed \u2014 pasted credentials frequently carry a
|
|
8056
|
+
// trailing newline or leading space that breaks URL construction
|
|
8057
|
+
// (zero-length identifier errors from the Postgres parser) or SCRAM
|
|
8058
|
+
// auth (silent password mismatch). Trim once, here, so every caller
|
|
8059
|
+
// benefits.
|
|
8060
|
+
var get = function (id) { return (document.getElementById(id).value || '').trim(); };
|
|
8015
8061
|
return {
|
|
8016
8062
|
type: 'postgres',
|
|
8017
|
-
label: (
|
|
8018
|
-
host: (
|
|
8063
|
+
label: get('w-label'),
|
|
8064
|
+
host: get('w-host'),
|
|
8019
8065
|
port: Number(document.getElementById('w-port').value || 5432),
|
|
8020
|
-
dbname: (
|
|
8021
|
-
user:
|
|
8022
|
-
password:
|
|
8066
|
+
dbname: get('w-dbname'),
|
|
8067
|
+
user: get('w-user'),
|
|
8068
|
+
password: get('w-password'),
|
|
8023
8069
|
};
|
|
8024
8070
|
}
|
|
8025
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.<project-ref></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.<project-ref>.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
|
+
|
|
8026
8147
|
function showMigrateToCloudModal(onClose) {
|
|
8027
8148
|
var bodyHtml =
|
|
8028
8149
|
'<p style="margin:0 0 12px;font-size:13px;color:var(--text-muted)">' +
|
|
@@ -8038,15 +8159,30 @@ var guiAppHtml = `<!doctype html>
|
|
|
8038
8159
|
onSubmit: function () {
|
|
8039
8160
|
var body = readPostgresWizardForm();
|
|
8040
8161
|
var msg = document.getElementById('w-msg');
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
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
|
+
});
|
|
8050
8186
|
},
|
|
8051
8187
|
});
|
|
8052
8188
|
}
|
|
@@ -8054,10 +8190,14 @@ var guiAppHtml = `<!doctype html>
|
|
|
8054
8190
|
function showConnectExistingModal(onClose) {
|
|
8055
8191
|
var bodyHtml =
|
|
8056
8192
|
'<p style="margin:0 0 12px;font-size:13px;color:var(--text-muted)">' +
|
|
8057
|
-
'
|
|
8058
|
-
'Your local SQLite
|
|
8059
|
-
'
|
|
8060
|
-
'
|
|
8193
|
+
'Switch this project to an <strong>existing</strong> cloud Postgres. ' +
|
|
8194
|
+
'Your local SQLite file is preserved \u2014 only this project\\'s active ' +
|
|
8195
|
+
'connection changes. Switch back any time by editing ' +
|
|
8196
|
+
'<code>lattice.config.yml</code>\\'s <code>db:</code> line or via the ' +
|
|
8197
|
+
'Databases catalog under User Config. If you want to <em>push</em> ' +
|
|
8198
|
+
'your local rows into the target instead, use Migrate to cloud. If ' +
|
|
8199
|
+
'the target is a teams DB you\\'ll be asked for an invite token ' +
|
|
8200
|
+
'after the probe.' +
|
|
8061
8201
|
'</p>' +
|
|
8062
8202
|
postgresFormHtml({}) +
|
|
8063
8203
|
'<div id="w-team-zone" style="margin-top:10px"></div>' +
|
|
@@ -8068,14 +8208,12 @@ var guiAppHtml = `<!doctype html>
|
|
|
8068
8208
|
onSubmit: function () {
|
|
8069
8209
|
var body = readPostgresWizardForm();
|
|
8070
8210
|
var msg = document.getElementById('w-msg');
|
|
8071
|
-
|
|
8072
|
-
//
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
.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)
|
|
8077
8216
|
.then(function (probe) {
|
|
8078
|
-
if (!probe.reachable) throw new Error('Unreachable: ' + (probe.error || 'unknown'));
|
|
8079
8217
|
if (probe.teamEnabled && !teamZoneShown) {
|
|
8080
8218
|
var zone = document.getElementById('w-team-zone');
|
|
8081
8219
|
zone.innerHTML =
|
|
@@ -9748,11 +9886,18 @@ async function probeCloud(targetUrl) {
|
|
|
9748
9886
|
}
|
|
9749
9887
|
return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
|
|
9750
9888
|
} catch (e) {
|
|
9889
|
+
const err = e;
|
|
9890
|
+
const parts = [];
|
|
9891
|
+
if (err.code) parts.push(`[${err.code}]`);
|
|
9892
|
+
if (err.message) parts.push(err.message);
|
|
9893
|
+
if (err.routine && !err.message.includes(err.routine)) {
|
|
9894
|
+
parts.push(`(routine: ${err.routine})`);
|
|
9895
|
+
}
|
|
9751
9896
|
return {
|
|
9752
9897
|
reachable: false,
|
|
9753
9898
|
dialect,
|
|
9754
9899
|
teamEnabled: false,
|
|
9755
|
-
error:
|
|
9900
|
+
error: parts.join(" ") || "unknown"
|
|
9756
9901
|
};
|
|
9757
9902
|
} finally {
|
|
9758
9903
|
if (probe) {
|
|
@@ -9764,6 +9909,86 @@ async function probeCloud(targetUrl) {
|
|
|
9764
9909
|
}
|
|
9765
9910
|
}
|
|
9766
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
|
+
|
|
9767
9992
|
// src/teams/client.ts
|
|
9768
9993
|
var TeamsClient = class {
|
|
9769
9994
|
constructor(local) {
|
|
@@ -9859,15 +10084,22 @@ var TeamsClient = class {
|
|
|
9859
10084
|
return { probe };
|
|
9860
10085
|
}
|
|
9861
10086
|
/**
|
|
9862
|
-
* Upgrade an already-connected cloud DB to a team DB.
|
|
9863
|
-
*
|
|
9864
|
-
*
|
|
9865
|
-
*
|
|
9866
|
-
*
|
|
9867
|
-
* `
|
|
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.
|
|
9868
10100
|
*/
|
|
9869
10101
|
async upgradeToTeamCloud(opts) {
|
|
9870
|
-
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);
|
|
9871
10103
|
writeToken(opts.label, reg.raw_token);
|
|
9872
10104
|
return reg;
|
|
9873
10105
|
}
|
|
@@ -11626,8 +11858,16 @@ async function openConfig(configPath, outputDir) {
|
|
|
11626
11858
|
db.define("__lattice_user_identity", {
|
|
11627
11859
|
columns: {
|
|
11628
11860
|
id: "TEXT PRIMARY KEY",
|
|
11629
|
-
|
|
11630
|
-
|
|
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 ''",
|
|
11631
11871
|
updated_at: "TEXT NOT NULL DEFAULT (datetime('now'))"
|
|
11632
11872
|
},
|
|
11633
11873
|
primaryKey: "id",
|
|
@@ -12103,7 +12343,20 @@ async function startGuiServer(options) {
|
|
|
12103
12343
|
sendJson5(res, { error: `Config not found: ${newPath}` }, 400);
|
|
12104
12344
|
return;
|
|
12105
12345
|
}
|
|
12106
|
-
|
|
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
|
+
}
|
|
12107
12360
|
active.db.close();
|
|
12108
12361
|
active = next;
|
|
12109
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
|
|
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: {
|
|
@@ -5730,11 +5865,18 @@ async function probeCloud(targetUrl) {
|
|
|
5730
5865
|
}
|
|
5731
5866
|
return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
|
|
5732
5867
|
} catch (e) {
|
|
5868
|
+
const err = e;
|
|
5869
|
+
const parts = [];
|
|
5870
|
+
if (err.code) parts.push(`[${err.code}]`);
|
|
5871
|
+
if (err.message) parts.push(err.message);
|
|
5872
|
+
if (err.routine && !err.message.includes(err.routine)) {
|
|
5873
|
+
parts.push(`(routine: ${err.routine})`);
|
|
5874
|
+
}
|
|
5733
5875
|
return {
|
|
5734
5876
|
reachable: false,
|
|
5735
5877
|
dialect,
|
|
5736
5878
|
teamEnabled: false,
|
|
5737
|
-
error:
|
|
5879
|
+
error: parts.join(" ") || "unknown"
|
|
5738
5880
|
};
|
|
5739
5881
|
} finally {
|
|
5740
5882
|
if (probe) {
|
|
@@ -5746,6 +5888,98 @@ async function probeCloud(targetUrl) {
|
|
|
5746
5888
|
}
|
|
5747
5889
|
}
|
|
5748
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
|
+
|
|
5749
5983
|
// src/teams/client.ts
|
|
5750
5984
|
var TeamsClient = class {
|
|
5751
5985
|
constructor(local) {
|
|
@@ -5841,15 +6075,22 @@ var TeamsClient = class {
|
|
|
5841
6075
|
return { probe };
|
|
5842
6076
|
}
|
|
5843
6077
|
/**
|
|
5844
|
-
* Upgrade an already-connected cloud DB to a team DB.
|
|
5845
|
-
*
|
|
5846
|
-
*
|
|
5847
|
-
*
|
|
5848
|
-
*
|
|
5849
|
-
* `
|
|
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.
|
|
5850
6091
|
*/
|
|
5851
6092
|
async upgradeToTeamCloud(opts) {
|
|
5852
|
-
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);
|
|
5853
6094
|
writeToken(opts.label, reg.raw_token);
|
|
5854
6095
|
return reg;
|
|
5855
6096
|
}
|
|
@@ -6154,7 +6395,7 @@ var TeamsClient = class {
|
|
|
6154
6395
|
if (conn.my_user_id !== link.owner_user_id) continue;
|
|
6155
6396
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6156
6397
|
await this.local.insert("__lattice_team_outbox", {
|
|
6157
|
-
id: (0,
|
|
6398
|
+
id: (0, import_node_crypto9.randomUUID)(),
|
|
6158
6399
|
team_id: link.team_id,
|
|
6159
6400
|
table_name: ctx.table,
|
|
6160
6401
|
pk: ctx.pk,
|
|
@@ -6263,7 +6504,7 @@ var TeamsClient = class {
|
|
|
6263
6504
|
totalApplied++;
|
|
6264
6505
|
} catch (e) {
|
|
6265
6506
|
await this.local.insert("__lattice_team_dlq", {
|
|
6266
|
-
id: (0,
|
|
6507
|
+
id: (0, import_node_crypto9.randomUUID)(),
|
|
6267
6508
|
team_id: connection.team_id,
|
|
6268
6509
|
envelope_json: JSON.stringify(env),
|
|
6269
6510
|
error: e.message,
|
|
@@ -6517,6 +6758,7 @@ function archiveLocalSqlite(dbPath) {
|
|
|
6517
6758
|
getOrCreateMasterKey,
|
|
6518
6759
|
hashFile,
|
|
6519
6760
|
isEncrypted,
|
|
6761
|
+
isPostgresUrl,
|
|
6520
6762
|
isV1EntityFiles,
|
|
6521
6763
|
listDbCredentials,
|
|
6522
6764
|
listTokens,
|
|
@@ -6534,6 +6776,7 @@ function archiveLocalSqlite(dbPath) {
|
|
|
6534
6776
|
readIdentity,
|
|
6535
6777
|
readManifest,
|
|
6536
6778
|
readToken,
|
|
6779
|
+
registerDirectViaPostgres,
|
|
6537
6780
|
registerNativeEntities,
|
|
6538
6781
|
saveDbCredential,
|
|
6539
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.
|
|
3021
|
-
*
|
|
3022
|
-
*
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3025
|
-
* `
|
|
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
|
-
|
|
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.
|
|
3021
|
-
*
|
|
3022
|
-
*
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3025
|
-
* `
|
|
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
|
-
|
|
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: {
|
|
@@ -5658,11 +5791,18 @@ async function probeCloud(targetUrl) {
|
|
|
5658
5791
|
}
|
|
5659
5792
|
return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
|
|
5660
5793
|
} catch (e) {
|
|
5794
|
+
const err = e;
|
|
5795
|
+
const parts = [];
|
|
5796
|
+
if (err.code) parts.push(`[${err.code}]`);
|
|
5797
|
+
if (err.message) parts.push(err.message);
|
|
5798
|
+
if (err.routine && !err.message.includes(err.routine)) {
|
|
5799
|
+
parts.push(`(routine: ${err.routine})`);
|
|
5800
|
+
}
|
|
5661
5801
|
return {
|
|
5662
5802
|
reachable: false,
|
|
5663
5803
|
dialect,
|
|
5664
5804
|
teamEnabled: false,
|
|
5665
|
-
error:
|
|
5805
|
+
error: parts.join(" ") || "unknown"
|
|
5666
5806
|
};
|
|
5667
5807
|
} finally {
|
|
5668
5808
|
if (probe) {
|
|
@@ -5674,6 +5814,98 @@ async function probeCloud(targetUrl) {
|
|
|
5674
5814
|
}
|
|
5675
5815
|
}
|
|
5676
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
|
+
|
|
5677
5909
|
// src/teams/client.ts
|
|
5678
5910
|
var TeamsClient = class {
|
|
5679
5911
|
constructor(local) {
|
|
@@ -5769,15 +6001,22 @@ var TeamsClient = class {
|
|
|
5769
6001
|
return { probe };
|
|
5770
6002
|
}
|
|
5771
6003
|
/**
|
|
5772
|
-
* Upgrade an already-connected cloud DB to a team DB.
|
|
5773
|
-
*
|
|
5774
|
-
*
|
|
5775
|
-
*
|
|
5776
|
-
*
|
|
5777
|
-
* `
|
|
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.
|
|
5778
6017
|
*/
|
|
5779
6018
|
async upgradeToTeamCloud(opts) {
|
|
5780
|
-
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);
|
|
5781
6020
|
writeToken(opts.label, reg.raw_token);
|
|
5782
6021
|
return reg;
|
|
5783
6022
|
}
|
|
@@ -6444,6 +6683,7 @@ export {
|
|
|
6444
6683
|
getOrCreateMasterKey,
|
|
6445
6684
|
hashFile,
|
|
6446
6685
|
isEncrypted,
|
|
6686
|
+
isPostgresUrl,
|
|
6447
6687
|
isV1EntityFiles,
|
|
6448
6688
|
listDbCredentials,
|
|
6449
6689
|
listTokens,
|
|
@@ -6461,6 +6701,7 @@ export {
|
|
|
6461
6701
|
readIdentity,
|
|
6462
6702
|
readManifest,
|
|
6463
6703
|
readToken,
|
|
6704
|
+
registerDirectViaPostgres,
|
|
6464
6705
|
registerNativeEntities,
|
|
6465
6706
|
saveDbCredential,
|
|
6466
6707
|
slugify,
|
package/package.json
CHANGED