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 +4 -0
- package/dist/cli.js +261 -31
- package/dist/index.cjs +246 -10
- package/dist/index.d.cts +64 -7
- package/dist/index.d.ts +64 -7
- package/dist/index.js +241 -7
- package/package.json +1 -1
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
|
-
|
|
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>' +
|
|
@@ -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.<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
|
+
|
|
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
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
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
|
-
|
|
8088
|
-
//
|
|
8089
|
-
|
|
8090
|
-
|
|
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.
|
|
9886
|
-
*
|
|
9887
|
-
*
|
|
9888
|
-
*
|
|
9889
|
-
*
|
|
9890
|
-
* `
|
|
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
|
-
|
|
11653
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
5852
|
-
*
|
|
5853
|
-
*
|
|
5854
|
-
*
|
|
5855
|
-
*
|
|
5856
|
-
* `
|
|
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,
|
|
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,
|
|
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.
|
|
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: {
|
|
@@ -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.
|
|
5780
|
-
*
|
|
5781
|
-
*
|
|
5782
|
-
*
|
|
5783
|
-
*
|
|
5784
|
-
* `
|
|
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