latticesql 1.13.3 → 1.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +224 -42
- package/dist/index.cjs +131 -5
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +131 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5943,6 +5943,30 @@ var guiAppHtml = `<!doctype html>
|
|
|
5943
5943
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
5944
5944
|
}
|
|
5945
5945
|
|
|
5946
|
+
// Redact the userinfo portion of a connection URL so the password
|
|
5947
|
+
// never reaches the rendered DOM. Used for every place the GUI
|
|
5948
|
+
// displays a cloud_url field (team cards, connection list, etc).
|
|
5949
|
+
// Defensive fallback returns the input as-is when it doesn't parse
|
|
5950
|
+
// as a URL \u2014 better to render a non-credential string verbatim than
|
|
5951
|
+
// to silently swallow the value.
|
|
5952
|
+
function redactUrlCredentials(url) {
|
|
5953
|
+
if (url == null) return '';
|
|
5954
|
+
var s = String(url);
|
|
5955
|
+
try {
|
|
5956
|
+
var u = new URL(s);
|
|
5957
|
+
if (u.password) {
|
|
5958
|
+
// Preserve the username (often useful for identification \u2014
|
|
5959
|
+
// e.g. tenant prefixes like postgres.<ref>) but mask the
|
|
5960
|
+
// password portion. URL.password is the decoded form.
|
|
5961
|
+
u.password = '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022';
|
|
5962
|
+
return u.toString();
|
|
5963
|
+
}
|
|
5964
|
+
return s;
|
|
5965
|
+
} catch (_) {
|
|
5966
|
+
return s;
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
|
|
5946
5970
|
function truncate(s, n) {
|
|
5947
5971
|
if (s == null) return '';
|
|
5948
5972
|
s = String(s);
|
|
@@ -7759,7 +7783,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
7759
7783
|
'<button class="btn danger-btn" data-act="signout">Sign out</button>' +
|
|
7760
7784
|
'</h3>' +
|
|
7761
7785
|
'<div class="team-meta">' +
|
|
7762
|
-
'Cloud: <code>' + escapeHtml(c.cloud_url) + '</code> \xB7 ' +
|
|
7786
|
+
'Cloud: <code>' + escapeHtml(redactUrlCredentials(c.cloud_url)) + '</code> \xB7 ' +
|
|
7763
7787
|
'User id: <code>' + escapeHtml(c.my_user_id) + '</code> \xB7 ' +
|
|
7764
7788
|
'Joined ' + escapeHtml(c.joined_at) +
|
|
7765
7789
|
'</div>' +
|
|
@@ -8281,37 +8305,76 @@ var guiAppHtml = `<!doctype html>
|
|
|
8281
8305
|
}
|
|
8282
8306
|
|
|
8283
8307
|
function renderTeamCard(card, conn) {
|
|
8284
|
-
// Fetch status + shared + members in parallel
|
|
8285
|
-
// non-creators (only members can
|
|
8308
|
+
// Fetch status + shared + members in parallel. The members fetch
|
|
8309
|
+
// can fail two ways: HTTP 403 for non-creators (only members can
|
|
8310
|
+
// list \u2014 though in practice every active member can), or the
|
|
8311
|
+
// cloud is genuinely unreachable. We track which case we're in so
|
|
8312
|
+
// the role pill says something more useful than "unknown".
|
|
8286
8313
|
var teamId = conn.team_id;
|
|
8314
|
+
var membersFailed = false;
|
|
8287
8315
|
Promise.all([
|
|
8288
8316
|
fetchJson('/api/teams-gui/teams/' + teamId + '/status'),
|
|
8289
8317
|
fetchJson('/api/teams-gui/teams/' + teamId + '/shared').catch(function () { return { objects: [] }; }),
|
|
8290
|
-
fetchJson('/api/teams-gui/teams/' + teamId + '/members').catch(function () {
|
|
8318
|
+
fetchJson('/api/teams-gui/teams/' + teamId + '/members').catch(function () {
|
|
8319
|
+
membersFailed = true;
|
|
8320
|
+
return { members: [] };
|
|
8321
|
+
}),
|
|
8291
8322
|
]).then(function (results) {
|
|
8292
8323
|
var status = results[0];
|
|
8293
8324
|
var shared = results[1].objects;
|
|
8294
8325
|
var members = results[2].members;
|
|
8295
8326
|
var myMembership = members.find(function (m) { return m.user_id === conn.my_user_id; });
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8327
|
+
// Resolve the role label with a three-step fallback chain that
|
|
8328
|
+
// distinguishes "we know what the role is" from "we couldn't ask
|
|
8329
|
+
// the cloud" from "the cloud answered but didn't recognize us".
|
|
8330
|
+
// The pre-v1.13.4 implementation collapsed all three into
|
|
8331
|
+
// "unknown" \u2014 confusing when (a) the user just created the
|
|
8332
|
+
// team and IS the creator on the cloud side, or (b) the cloud
|
|
8333
|
+
// briefly hiccupped. Use saveConnection's own data as the
|
|
8334
|
+
// authoritative source when we have it on the local row.
|
|
8335
|
+
var roleLabel;
|
|
8336
|
+
var isCreator;
|
|
8337
|
+
if (myMembership) {
|
|
8338
|
+
roleLabel = myMembership.role;
|
|
8339
|
+
isCreator = myMembership.role === 'creator';
|
|
8340
|
+
} else if (membersFailed) {
|
|
8341
|
+
// Cloud listMembers couldn't be reached. We don't know who
|
|
8342
|
+
// we are remotely, but the local connection row knows we
|
|
8343
|
+
// joined this team \u2014 surface that, with a soft warning.
|
|
8344
|
+
roleLabel = '(cloud unreachable)';
|
|
8345
|
+
isCreator = false;
|
|
8346
|
+
} else {
|
|
8347
|
+
// listMembers returned a list, but our user_id wasn't in it.
|
|
8348
|
+
// Either we were kicked or the local my_user_id is stale.
|
|
8349
|
+
roleLabel = '(not in member list)';
|
|
8350
|
+
isCreator = false;
|
|
8351
|
+
}
|
|
8352
|
+
var rolePill = '<span class="role-tag' + (isCreator ? '' : ' role-member') + '">' +
|
|
8353
|
+
escapeHtml(roleLabel) +
|
|
8354
|
+
'</span>';
|
|
8355
|
+
|
|
8356
|
+
// v1.13.4: no manual-sync button and no Last seq / Outbox / DLQ /
|
|
8357
|
+
// Local links stats. Lattice is realtime against its canonical
|
|
8358
|
+
// store \u2014 every read and every write the GUI does hits the
|
|
8359
|
+
// active DB directly. When the project's db: line points at a
|
|
8360
|
+
// Postgres URL, that IS the cloud DB; when it's local SQLite,
|
|
8361
|
+
// it's the canonical local. There's nothing for the user to
|
|
8362
|
+
// "sync" \u2014 operations either succeed live or fail gracefully
|
|
8363
|
+
// when the connection is down. The outbox/change-log machinery
|
|
8364
|
+
// is an HTTP-mode-only internal that the CLI still exposes
|
|
8365
|
+
// (lattice teams sync) for power users; the GUI no longer
|
|
8366
|
+
// pretends it's a user-facing action.
|
|
8367
|
+
// Reference status once so eslint no-unused-vars stays happy
|
|
8368
|
+
// even though we've intentionally dropped its display.
|
|
8369
|
+
void status;
|
|
8300
8370
|
card.innerHTML =
|
|
8301
8371
|
'<h3>' + escapeHtml(conn.team_name) + ' ' + rolePill +
|
|
8302
|
-
'<span style="font-size:11px;color:var(--text-muted);font-weight:normal">' + escapeHtml(conn.cloud_url) + '</span>' +
|
|
8372
|
+
'<span style="font-size:11px;color:var(--text-muted);font-weight:normal">' + escapeHtml(redactUrlCredentials(conn.cloud_url)) + '</span>' +
|
|
8303
8373
|
'</h3>' +
|
|
8304
8374
|
'<div class="team-meta">team-id: <code>' + escapeHtml(teamId) + '</code></div>' +
|
|
8305
|
-
'<div class="team-stats">' +
|
|
8306
|
-
'<div class="team-stat"><div class="stat-label">Last seq</div><div class="stat-value">' + lastSeq + '</div></div>' +
|
|
8307
|
-
'<div class="team-stat"><div class="stat-label">Outbox</div><div class="stat-value">' + status.outbox_depth + '</div></div>' +
|
|
8308
|
-
'<div class="team-stat"><div class="stat-label">DLQ</div><div class="stat-value">' + status.dlq_depth + '</div></div>' +
|
|
8309
|
-
'<div class="team-stat"><div class="stat-label">Local links</div><div class="stat-value">' + status.local_links + '</div></div>' +
|
|
8310
|
-
'</div>' +
|
|
8311
8375
|
'<div class="team-actions">' +
|
|
8312
|
-
'<button class="btn primary" data-act="sync">Sync now</button>' +
|
|
8313
8376
|
(isCreator
|
|
8314
|
-
? '<button class="btn" data-act="invite">Generate invite token</button>'
|
|
8377
|
+
? '<button class="btn primary" data-act="invite">Generate invite token</button>'
|
|
8315
8378
|
: '') +
|
|
8316
8379
|
'<button class="btn" data-act="leave">' + (isCreator ? 'Destroy team' : 'Leave team') + '</button>' +
|
|
8317
8380
|
'</div>' +
|
|
@@ -8359,11 +8422,9 @@ var guiAppHtml = `<!doctype html>
|
|
|
8359
8422
|
|
|
8360
8423
|
function wireTeamCardActions(card, conn, isCreator) {
|
|
8361
8424
|
var teamId = conn.team_id;
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
.catch(function (err) { alert('Sync failed: ' + err.message); });
|
|
8366
|
-
});
|
|
8425
|
+
// v1.13.4: no Sync button anymore. The CLI command lattice teams
|
|
8426
|
+
// sync remains for HTTP-mode operators who need to nudge their
|
|
8427
|
+
// outbox; the GUI is realtime against the canonical store.
|
|
8367
8428
|
var inviteBtn = card.querySelector('[data-act="invite"]');
|
|
8368
8429
|
if (inviteBtn) inviteBtn.addEventListener('click', function () {
|
|
8369
8430
|
showInviteByEmailModal(teamId);
|
|
@@ -9989,6 +10050,74 @@ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
|
|
|
9989
10050
|
}
|
|
9990
10051
|
}
|
|
9991
10052
|
|
|
10053
|
+
// src/teams/direct-ops.ts
|
|
10054
|
+
async function listMembersDirect(db, teamId) {
|
|
10055
|
+
const members = await db.query("__lattice_team_members", {
|
|
10056
|
+
filters: [{ col: "team_id", op: "eq", val: teamId }]
|
|
10057
|
+
});
|
|
10058
|
+
if (members.length === 0) return [];
|
|
10059
|
+
const users = await db.query("__lattice_users", {
|
|
10060
|
+
filters: [
|
|
10061
|
+
{ col: "id", op: "in", val: members.map((m) => m.user_id) },
|
|
10062
|
+
{ col: "deleted_at", op: "isNull" }
|
|
10063
|
+
]
|
|
10064
|
+
});
|
|
10065
|
+
const userById = new Map(users.map((u) => [u.id, u]));
|
|
10066
|
+
const out = [];
|
|
10067
|
+
for (const m of members) {
|
|
10068
|
+
const u = userById.get(m.user_id);
|
|
10069
|
+
if (!u) continue;
|
|
10070
|
+
out.push({
|
|
10071
|
+
user_id: m.user_id,
|
|
10072
|
+
email: u.email,
|
|
10073
|
+
name: u.name,
|
|
10074
|
+
role: m.role,
|
|
10075
|
+
joined_at: m.joined_at
|
|
10076
|
+
});
|
|
10077
|
+
}
|
|
10078
|
+
return out;
|
|
10079
|
+
}
|
|
10080
|
+
async function inviteDirect(db, teamId, inviterUserId, inviteeEmail, expiresInHours = 7 * 24) {
|
|
10081
|
+
const team = await db.get("__lattice_team", teamId);
|
|
10082
|
+
if (!team || team.deleted_at) {
|
|
10083
|
+
throw new Error(`Team not found: ${teamId}`);
|
|
10084
|
+
}
|
|
10085
|
+
const expiresAt = new Date(Date.now() + expiresInHours * 36e5).toISOString();
|
|
10086
|
+
const { raw, hash } = generateInviteToken();
|
|
10087
|
+
const id = await db.insert("__lattice_invitations", {
|
|
10088
|
+
team_id: teamId,
|
|
10089
|
+
token_hash: hash,
|
|
10090
|
+
invitee_email: inviteeEmail,
|
|
10091
|
+
invited_by_user_id: inviterUserId,
|
|
10092
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10093
|
+
expires_at: expiresAt
|
|
10094
|
+
});
|
|
10095
|
+
return {
|
|
10096
|
+
id,
|
|
10097
|
+
raw_token: raw,
|
|
10098
|
+
expires_at: expiresAt,
|
|
10099
|
+
team_name: team.name,
|
|
10100
|
+
invitee_email: inviteeEmail
|
|
10101
|
+
};
|
|
10102
|
+
}
|
|
10103
|
+
async function kickMemberDirect(db, teamId, userId) {
|
|
10104
|
+
await db.delete("__lattice_team_members", { team_id: teamId, user_id: userId });
|
|
10105
|
+
}
|
|
10106
|
+
async function destroyTeamDirect(db) {
|
|
10107
|
+
const identityRow = await db.get("__lattice_team_identity", "singleton");
|
|
10108
|
+
if (identityRow && typeof identityRow.team_id === "string") {
|
|
10109
|
+
const teamId = identityRow.team_id;
|
|
10110
|
+
const members = await db.query("__lattice_team_members", {
|
|
10111
|
+
filters: [{ col: "team_id", op: "eq", val: teamId }]
|
|
10112
|
+
});
|
|
10113
|
+
for (const m of members) {
|
|
10114
|
+
await db.delete("__lattice_team_members", { team_id: teamId, user_id: m.user_id });
|
|
10115
|
+
}
|
|
10116
|
+
await db.update("__lattice_team", teamId, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10117
|
+
}
|
|
10118
|
+
await db.delete("__lattice_team_identity", "singleton");
|
|
10119
|
+
}
|
|
10120
|
+
|
|
9992
10121
|
// src/teams/client.ts
|
|
9993
10122
|
var TeamsClient = class {
|
|
9994
10123
|
constructor(local) {
|
|
@@ -10075,6 +10204,13 @@ var TeamsClient = class {
|
|
|
10075
10204
|
const redeem = await this.redeemInvite(opts.cloudUrl, inviteToken, email, name);
|
|
10076
10205
|
saveDbCredential(opts.label, opts.cloudUrl);
|
|
10077
10206
|
writeToken(opts.label, redeem.raw_token);
|
|
10207
|
+
await this.saveConnection({
|
|
10208
|
+
team_id: redeem.team.id,
|
|
10209
|
+
team_name: redeem.team.name,
|
|
10210
|
+
cloud_url: opts.cloudUrl,
|
|
10211
|
+
my_user_id: redeem.user.id,
|
|
10212
|
+
api_token: redeem.raw_token
|
|
10213
|
+
});
|
|
10078
10214
|
return {
|
|
10079
10215
|
probe,
|
|
10080
10216
|
joinedAsMember: { user_id: redeem.user.id, team_id: redeem.team.id }
|
|
@@ -10094,21 +10230,51 @@ var TeamsClient = class {
|
|
|
10094
10230
|
* The HTTP path can't be used here because the browser's Fetch
|
|
10095
10231
|
* API refuses URLs with embedded credentials.
|
|
10096
10232
|
*
|
|
10097
|
-
* On success writes the bearer token to `~/.lattice/keys/<label>.token
|
|
10098
|
-
*
|
|
10099
|
-
*
|
|
10233
|
+
* On success writes the bearer token to `~/.lattice/keys/<label>.token`
|
|
10234
|
+
* **and** persists the local `__lattice_team_connections` row so the
|
|
10235
|
+
* GUI's team-management API calls can authenticate immediately
|
|
10236
|
+
* afterward (members, invites, kick, destroy). v1.13.4 added the
|
|
10237
|
+
* connection-row write — the older v1.13 implementation only wrote
|
|
10238
|
+
* the token file, leaving GUI authenticated calls with no
|
|
10239
|
+
* `cloud_url` + `my_user_id` + `api_token_encrypted` row to read.
|
|
10100
10240
|
*/
|
|
10101
10241
|
async upgradeToTeamCloud(opts) {
|
|
10102
10242
|
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);
|
|
10103
10243
|
writeToken(opts.label, reg.raw_token);
|
|
10244
|
+
await this.saveConnection({
|
|
10245
|
+
team_id: reg.team.id,
|
|
10246
|
+
team_name: reg.team.name,
|
|
10247
|
+
cloud_url: opts.cloudUrl,
|
|
10248
|
+
my_user_id: reg.user.id,
|
|
10249
|
+
api_token: reg.raw_token
|
|
10250
|
+
});
|
|
10104
10251
|
return reg;
|
|
10105
10252
|
}
|
|
10106
|
-
// ── Cloud
|
|
10253
|
+
// ── Cloud team operations (dispatch on URL scheme) ──────────────────────
|
|
10254
|
+
// For HTTP cloud URLs (`http://lattice-server:port`), every operation
|
|
10255
|
+
// round-trips through the team server's authenticated REST API. For
|
|
10256
|
+
// direct-Postgres cloud URLs (`postgres://...`), the user's `this.local`
|
|
10257
|
+
// Lattice IS the cloud DB — operations dispatch through `direct-ops.ts`
|
|
10258
|
+
// helpers that run the same INSERT / UPDATE / DELETE / SELECT logic
|
|
10259
|
+
// against `this.local` directly. The Fetch API can't handle
|
|
10260
|
+
// credentials-in-URL anyway, so the dispatch isn't optional.
|
|
10261
|
+
//
|
|
10262
|
+
// Authorization model: for HTTP clouds, the server gates by bearer
|
|
10263
|
+
// token + membership row. For direct-Postgres, possession of the
|
|
10264
|
+
// connection credential is the implicit gate — the operator is
|
|
10265
|
+
// already reading/writing the canonical data.
|
|
10107
10266
|
/** Destroy the singleton team. Creator-only on the cloud side. */
|
|
10108
10267
|
async destroyTeam(cloudUrl, token) {
|
|
10268
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10269
|
+
await destroyTeamDirect(this.local);
|
|
10270
|
+
return;
|
|
10271
|
+
}
|
|
10109
10272
|
await this.fetchAuthed(cloudUrl, token, "DELETE", "/api/team");
|
|
10110
10273
|
}
|
|
10111
10274
|
async listMembers(cloudUrl, token, teamId) {
|
|
10275
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10276
|
+
return listMembersDirect(this.local, teamId);
|
|
10277
|
+
}
|
|
10112
10278
|
const r = await this.fetchAuthed(
|
|
10113
10279
|
cloudUrl,
|
|
10114
10280
|
token,
|
|
@@ -10117,7 +10283,15 @@ var TeamsClient = class {
|
|
|
10117
10283
|
);
|
|
10118
10284
|
return r.members;
|
|
10119
10285
|
}
|
|
10120
|
-
async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours) {
|
|
10286
|
+
async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours, inviterUserId) {
|
|
10287
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10288
|
+
if (!inviterUserId) {
|
|
10289
|
+
throw new Error(
|
|
10290
|
+
"invite: inviterUserId is required for direct-Postgres cloud URLs (read it from __lattice_team_connections.my_user_id)"
|
|
10291
|
+
);
|
|
10292
|
+
}
|
|
10293
|
+
return inviteDirect(this.local, teamId, inviterUserId, inviteeEmail, expiresInHours);
|
|
10294
|
+
}
|
|
10121
10295
|
const body = { invitee_email: inviteeEmail };
|
|
10122
10296
|
if (expiresInHours !== void 0) body.expires_in_hours = expiresInHours;
|
|
10123
10297
|
return this.fetchAuthed(
|
|
@@ -10129,6 +10303,10 @@ var TeamsClient = class {
|
|
|
10129
10303
|
);
|
|
10130
10304
|
}
|
|
10131
10305
|
async kickMember(cloudUrl, token, teamId, userId) {
|
|
10306
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10307
|
+
await kickMemberDirect(this.local, teamId, userId);
|
|
10308
|
+
return;
|
|
10309
|
+
}
|
|
10132
10310
|
await this.fetchAuthed(
|
|
10133
10311
|
cloudUrl,
|
|
10134
10312
|
token,
|
|
@@ -10182,6 +10360,9 @@ var TeamsClient = class {
|
|
|
10182
10360
|
* `has_more` flag tells callers to loop until drained before sleeping.
|
|
10183
10361
|
*/
|
|
10184
10362
|
async fetchChangeBatch(cloudUrl, token, teamId, since = 0, limit = 500) {
|
|
10363
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
10364
|
+
return { envelopes: [], has_more: false };
|
|
10365
|
+
}
|
|
10185
10366
|
return this.fetchAuthed(
|
|
10186
10367
|
cloudUrl,
|
|
10187
10368
|
token,
|
|
@@ -10789,7 +10970,8 @@ async function dispatchTeamSubroute(req, res, ctx, teamId, subpath) {
|
|
|
10789
10970
|
conn.api_token,
|
|
10790
10971
|
teamId,
|
|
10791
10972
|
inviteeEmail,
|
|
10792
|
-
hours
|
|
10973
|
+
hours,
|
|
10974
|
+
conn.my_user_id
|
|
10793
10975
|
);
|
|
10794
10976
|
sendJson2(res, invite);
|
|
10795
10977
|
return;
|
|
@@ -12282,23 +12464,23 @@ async function startGuiServer(options) {
|
|
|
12282
12464
|
return;
|
|
12283
12465
|
}
|
|
12284
12466
|
if (method === "GET" && pathname === "/api/system-tables") {
|
|
12285
|
-
const
|
|
12286
|
-
|
|
12287
|
-
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12467
|
+
const adapter = active.db._adapter;
|
|
12468
|
+
let rows = [];
|
|
12469
|
+
if (adapter.allAsync) {
|
|
12470
|
+
const listSql = adapter.dialect === "postgres" ? (
|
|
12471
|
+
// pg_tables is the public-schema-only counterpart to
|
|
12472
|
+
// sqlite_master. We filter to public + the same `\_%`
|
|
12473
|
+
// ESCAPE pattern so the result is identical to SQLite.
|
|
12474
|
+
// Underscore is a LIKE wildcard in both engines.
|
|
12475
|
+
`SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE '\\_%' ESCAPE '\\' ORDER BY tablename`
|
|
12476
|
+
) : `SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '\\_%' ESCAPE '\\' ORDER BY name`;
|
|
12477
|
+
rows = await adapter.allAsync(listSql);
|
|
12478
|
+
}
|
|
12294
12479
|
const tables = [];
|
|
12295
12480
|
for (const r of rows) {
|
|
12296
|
-
const cols = await (
|
|
12297
|
-
const adapter = active.db._adapter;
|
|
12298
|
-
return adapter.allAsync?.(`PRAGMA table_info("${r.name}")`) ?? Promise.resolve([]);
|
|
12299
|
-
})();
|
|
12481
|
+
const cols = await active.db.introspectColumns(r.name);
|
|
12300
12482
|
const rowCount = await active.db.count(r.name);
|
|
12301
|
-
tables.push({ name: r.name, columns: cols
|
|
12483
|
+
tables.push({ name: r.name, columns: cols, rowCount });
|
|
12302
12484
|
}
|
|
12303
12485
|
sendJson5(res, { tables });
|
|
12304
12486
|
return;
|
package/dist/index.cjs
CHANGED
|
@@ -5891,7 +5891,9 @@ async function probeCloud(targetUrl) {
|
|
|
5891
5891
|
// src/teams/server/auth.ts
|
|
5892
5892
|
var import_node_crypto8 = require("crypto");
|
|
5893
5893
|
var TOKEN_PREFIX = "lat_";
|
|
5894
|
+
var INVITE_PREFIX = "latinv_";
|
|
5894
5895
|
var TOKEN_BYTES = 32;
|
|
5896
|
+
var INVITE_BYTES = 24;
|
|
5895
5897
|
function hashToken(rawToken) {
|
|
5896
5898
|
return (0, import_node_crypto8.createHash)("sha256").update(rawToken).digest("hex");
|
|
5897
5899
|
}
|
|
@@ -5899,6 +5901,10 @@ function generateToken() {
|
|
|
5899
5901
|
const raw = `${TOKEN_PREFIX}${(0, import_node_crypto8.randomBytes)(TOKEN_BYTES).toString("hex")}`;
|
|
5900
5902
|
return { raw, hash: hashToken(raw) };
|
|
5901
5903
|
}
|
|
5904
|
+
function generateInviteToken() {
|
|
5905
|
+
const raw = `${INVITE_PREFIX}${(0, import_node_crypto8.randomBytes)(INVITE_BYTES).toString("hex")}`;
|
|
5906
|
+
return { raw, hash: hashToken(raw) };
|
|
5907
|
+
}
|
|
5902
5908
|
|
|
5903
5909
|
// src/teams/register-direct.ts
|
|
5904
5910
|
function isPostgresUrl(url) {
|
|
@@ -5980,6 +5986,74 @@ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
|
|
|
5980
5986
|
}
|
|
5981
5987
|
}
|
|
5982
5988
|
|
|
5989
|
+
// src/teams/direct-ops.ts
|
|
5990
|
+
async function listMembersDirect(db, teamId) {
|
|
5991
|
+
const members = await db.query("__lattice_team_members", {
|
|
5992
|
+
filters: [{ col: "team_id", op: "eq", val: teamId }]
|
|
5993
|
+
});
|
|
5994
|
+
if (members.length === 0) return [];
|
|
5995
|
+
const users = await db.query("__lattice_users", {
|
|
5996
|
+
filters: [
|
|
5997
|
+
{ col: "id", op: "in", val: members.map((m) => m.user_id) },
|
|
5998
|
+
{ col: "deleted_at", op: "isNull" }
|
|
5999
|
+
]
|
|
6000
|
+
});
|
|
6001
|
+
const userById = new Map(users.map((u) => [u.id, u]));
|
|
6002
|
+
const out = [];
|
|
6003
|
+
for (const m of members) {
|
|
6004
|
+
const u = userById.get(m.user_id);
|
|
6005
|
+
if (!u) continue;
|
|
6006
|
+
out.push({
|
|
6007
|
+
user_id: m.user_id,
|
|
6008
|
+
email: u.email,
|
|
6009
|
+
name: u.name,
|
|
6010
|
+
role: m.role,
|
|
6011
|
+
joined_at: m.joined_at
|
|
6012
|
+
});
|
|
6013
|
+
}
|
|
6014
|
+
return out;
|
|
6015
|
+
}
|
|
6016
|
+
async function inviteDirect(db, teamId, inviterUserId, inviteeEmail, expiresInHours = 7 * 24) {
|
|
6017
|
+
const team = await db.get("__lattice_team", teamId);
|
|
6018
|
+
if (!team || team.deleted_at) {
|
|
6019
|
+
throw new Error(`Team not found: ${teamId}`);
|
|
6020
|
+
}
|
|
6021
|
+
const expiresAt = new Date(Date.now() + expiresInHours * 36e5).toISOString();
|
|
6022
|
+
const { raw, hash } = generateInviteToken();
|
|
6023
|
+
const id = await db.insert("__lattice_invitations", {
|
|
6024
|
+
team_id: teamId,
|
|
6025
|
+
token_hash: hash,
|
|
6026
|
+
invitee_email: inviteeEmail,
|
|
6027
|
+
invited_by_user_id: inviterUserId,
|
|
6028
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6029
|
+
expires_at: expiresAt
|
|
6030
|
+
});
|
|
6031
|
+
return {
|
|
6032
|
+
id,
|
|
6033
|
+
raw_token: raw,
|
|
6034
|
+
expires_at: expiresAt,
|
|
6035
|
+
team_name: team.name,
|
|
6036
|
+
invitee_email: inviteeEmail
|
|
6037
|
+
};
|
|
6038
|
+
}
|
|
6039
|
+
async function kickMemberDirect(db, teamId, userId) {
|
|
6040
|
+
await db.delete("__lattice_team_members", { team_id: teamId, user_id: userId });
|
|
6041
|
+
}
|
|
6042
|
+
async function destroyTeamDirect(db) {
|
|
6043
|
+
const identityRow = await db.get("__lattice_team_identity", "singleton");
|
|
6044
|
+
if (identityRow && typeof identityRow.team_id === "string") {
|
|
6045
|
+
const teamId = identityRow.team_id;
|
|
6046
|
+
const members = await db.query("__lattice_team_members", {
|
|
6047
|
+
filters: [{ col: "team_id", op: "eq", val: teamId }]
|
|
6048
|
+
});
|
|
6049
|
+
for (const m of members) {
|
|
6050
|
+
await db.delete("__lattice_team_members", { team_id: teamId, user_id: m.user_id });
|
|
6051
|
+
}
|
|
6052
|
+
await db.update("__lattice_team", teamId, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6053
|
+
}
|
|
6054
|
+
await db.delete("__lattice_team_identity", "singleton");
|
|
6055
|
+
}
|
|
6056
|
+
|
|
5983
6057
|
// src/teams/client.ts
|
|
5984
6058
|
var TeamsClient = class {
|
|
5985
6059
|
constructor(local) {
|
|
@@ -6066,6 +6140,13 @@ var TeamsClient = class {
|
|
|
6066
6140
|
const redeem = await this.redeemInvite(opts.cloudUrl, inviteToken, email, name);
|
|
6067
6141
|
saveDbCredential(opts.label, opts.cloudUrl);
|
|
6068
6142
|
writeToken(opts.label, redeem.raw_token);
|
|
6143
|
+
await this.saveConnection({
|
|
6144
|
+
team_id: redeem.team.id,
|
|
6145
|
+
team_name: redeem.team.name,
|
|
6146
|
+
cloud_url: opts.cloudUrl,
|
|
6147
|
+
my_user_id: redeem.user.id,
|
|
6148
|
+
api_token: redeem.raw_token
|
|
6149
|
+
});
|
|
6069
6150
|
return {
|
|
6070
6151
|
probe,
|
|
6071
6152
|
joinedAsMember: { user_id: redeem.user.id, team_id: redeem.team.id }
|
|
@@ -6085,21 +6166,51 @@ var TeamsClient = class {
|
|
|
6085
6166
|
* The HTTP path can't be used here because the browser's Fetch
|
|
6086
6167
|
* API refuses URLs with embedded credentials.
|
|
6087
6168
|
*
|
|
6088
|
-
* On success writes the bearer token to `~/.lattice/keys/<label>.token
|
|
6089
|
-
*
|
|
6090
|
-
*
|
|
6169
|
+
* On success writes the bearer token to `~/.lattice/keys/<label>.token`
|
|
6170
|
+
* **and** persists the local `__lattice_team_connections` row so the
|
|
6171
|
+
* GUI's team-management API calls can authenticate immediately
|
|
6172
|
+
* afterward (members, invites, kick, destroy). v1.13.4 added the
|
|
6173
|
+
* connection-row write — the older v1.13 implementation only wrote
|
|
6174
|
+
* the token file, leaving GUI authenticated calls with no
|
|
6175
|
+
* `cloud_url` + `my_user_id` + `api_token_encrypted` row to read.
|
|
6091
6176
|
*/
|
|
6092
6177
|
async upgradeToTeamCloud(opts) {
|
|
6093
6178
|
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);
|
|
6094
6179
|
writeToken(opts.label, reg.raw_token);
|
|
6180
|
+
await this.saveConnection({
|
|
6181
|
+
team_id: reg.team.id,
|
|
6182
|
+
team_name: reg.team.name,
|
|
6183
|
+
cloud_url: opts.cloudUrl,
|
|
6184
|
+
my_user_id: reg.user.id,
|
|
6185
|
+
api_token: reg.raw_token
|
|
6186
|
+
});
|
|
6095
6187
|
return reg;
|
|
6096
6188
|
}
|
|
6097
|
-
// ── Cloud
|
|
6189
|
+
// ── Cloud team operations (dispatch on URL scheme) ──────────────────────
|
|
6190
|
+
// For HTTP cloud URLs (`http://lattice-server:port`), every operation
|
|
6191
|
+
// round-trips through the team server's authenticated REST API. For
|
|
6192
|
+
// direct-Postgres cloud URLs (`postgres://...`), the user's `this.local`
|
|
6193
|
+
// Lattice IS the cloud DB — operations dispatch through `direct-ops.ts`
|
|
6194
|
+
// helpers that run the same INSERT / UPDATE / DELETE / SELECT logic
|
|
6195
|
+
// against `this.local` directly. The Fetch API can't handle
|
|
6196
|
+
// credentials-in-URL anyway, so the dispatch isn't optional.
|
|
6197
|
+
//
|
|
6198
|
+
// Authorization model: for HTTP clouds, the server gates by bearer
|
|
6199
|
+
// token + membership row. For direct-Postgres, possession of the
|
|
6200
|
+
// connection credential is the implicit gate — the operator is
|
|
6201
|
+
// already reading/writing the canonical data.
|
|
6098
6202
|
/** Destroy the singleton team. Creator-only on the cloud side. */
|
|
6099
6203
|
async destroyTeam(cloudUrl, token) {
|
|
6204
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6205
|
+
await destroyTeamDirect(this.local);
|
|
6206
|
+
return;
|
|
6207
|
+
}
|
|
6100
6208
|
await this.fetchAuthed(cloudUrl, token, "DELETE", "/api/team");
|
|
6101
6209
|
}
|
|
6102
6210
|
async listMembers(cloudUrl, token, teamId) {
|
|
6211
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6212
|
+
return listMembersDirect(this.local, teamId);
|
|
6213
|
+
}
|
|
6103
6214
|
const r = await this.fetchAuthed(
|
|
6104
6215
|
cloudUrl,
|
|
6105
6216
|
token,
|
|
@@ -6108,7 +6219,15 @@ var TeamsClient = class {
|
|
|
6108
6219
|
);
|
|
6109
6220
|
return r.members;
|
|
6110
6221
|
}
|
|
6111
|
-
async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours) {
|
|
6222
|
+
async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours, inviterUserId) {
|
|
6223
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6224
|
+
if (!inviterUserId) {
|
|
6225
|
+
throw new Error(
|
|
6226
|
+
"invite: inviterUserId is required for direct-Postgres cloud URLs (read it from __lattice_team_connections.my_user_id)"
|
|
6227
|
+
);
|
|
6228
|
+
}
|
|
6229
|
+
return inviteDirect(this.local, teamId, inviterUserId, inviteeEmail, expiresInHours);
|
|
6230
|
+
}
|
|
6112
6231
|
const body = { invitee_email: inviteeEmail };
|
|
6113
6232
|
if (expiresInHours !== void 0) body.expires_in_hours = expiresInHours;
|
|
6114
6233
|
return this.fetchAuthed(
|
|
@@ -6120,6 +6239,10 @@ var TeamsClient = class {
|
|
|
6120
6239
|
);
|
|
6121
6240
|
}
|
|
6122
6241
|
async kickMember(cloudUrl, token, teamId, userId) {
|
|
6242
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6243
|
+
await kickMemberDirect(this.local, teamId, userId);
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6123
6246
|
await this.fetchAuthed(
|
|
6124
6247
|
cloudUrl,
|
|
6125
6248
|
token,
|
|
@@ -6173,6 +6296,9 @@ var TeamsClient = class {
|
|
|
6173
6296
|
* `has_more` flag tells callers to loop until drained before sleeping.
|
|
6174
6297
|
*/
|
|
6175
6298
|
async fetchChangeBatch(cloudUrl, token, teamId, since = 0, limit = 500) {
|
|
6299
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6300
|
+
return { envelopes: [], has_more: false };
|
|
6301
|
+
}
|
|
6176
6302
|
return this.fetchAuthed(
|
|
6177
6303
|
cloudUrl,
|
|
6178
6304
|
token,
|
package/dist/index.d.cts
CHANGED
|
@@ -3027,9 +3027,13 @@ declare class TeamsClient {
|
|
|
3027
3027
|
* The HTTP path can't be used here because the browser's Fetch
|
|
3028
3028
|
* API refuses URLs with embedded credentials.
|
|
3029
3029
|
*
|
|
3030
|
-
* On success writes the bearer token to `~/.lattice/keys/<label>.token
|
|
3031
|
-
*
|
|
3032
|
-
*
|
|
3030
|
+
* On success writes the bearer token to `~/.lattice/keys/<label>.token`
|
|
3031
|
+
* **and** persists the local `__lattice_team_connections` row so the
|
|
3032
|
+
* GUI's team-management API calls can authenticate immediately
|
|
3033
|
+
* afterward (members, invites, kick, destroy). v1.13.4 added the
|
|
3034
|
+
* connection-row write — the older v1.13 implementation only wrote
|
|
3035
|
+
* the token file, leaving GUI authenticated calls with no
|
|
3036
|
+
* `cloud_url` + `my_user_id` + `api_token_encrypted` row to read.
|
|
3033
3037
|
*/
|
|
3034
3038
|
upgradeToTeamCloud(opts: {
|
|
3035
3039
|
label: string;
|
|
@@ -3041,7 +3045,7 @@ declare class TeamsClient {
|
|
|
3041
3045
|
/** Destroy the singleton team. Creator-only on the cloud side. */
|
|
3042
3046
|
destroyTeam(cloudUrl: string, token: string): Promise<void>;
|
|
3043
3047
|
listMembers(cloudUrl: string, token: string, teamId: string): Promise<MemberSummary[]>;
|
|
3044
|
-
invite(cloudUrl: string, token: string, teamId: string, inviteeEmail: string, expiresInHours?: number): Promise<InviteResponse>;
|
|
3048
|
+
invite(cloudUrl: string, token: string, teamId: string, inviteeEmail: string, expiresInHours?: number, inviterUserId?: string): Promise<InviteResponse>;
|
|
3045
3049
|
kickMember(cloudUrl: string, token: string, teamId: string, userId: string): Promise<void>;
|
|
3046
3050
|
me(cloudUrl: string, token: string): Promise<{
|
|
3047
3051
|
user: {
|
package/dist/index.d.ts
CHANGED
|
@@ -3027,9 +3027,13 @@ declare class TeamsClient {
|
|
|
3027
3027
|
* The HTTP path can't be used here because the browser's Fetch
|
|
3028
3028
|
* API refuses URLs with embedded credentials.
|
|
3029
3029
|
*
|
|
3030
|
-
* On success writes the bearer token to `~/.lattice/keys/<label>.token
|
|
3031
|
-
*
|
|
3032
|
-
*
|
|
3030
|
+
* On success writes the bearer token to `~/.lattice/keys/<label>.token`
|
|
3031
|
+
* **and** persists the local `__lattice_team_connections` row so the
|
|
3032
|
+
* GUI's team-management API calls can authenticate immediately
|
|
3033
|
+
* afterward (members, invites, kick, destroy). v1.13.4 added the
|
|
3034
|
+
* connection-row write — the older v1.13 implementation only wrote
|
|
3035
|
+
* the token file, leaving GUI authenticated calls with no
|
|
3036
|
+
* `cloud_url` + `my_user_id` + `api_token_encrypted` row to read.
|
|
3033
3037
|
*/
|
|
3034
3038
|
upgradeToTeamCloud(opts: {
|
|
3035
3039
|
label: string;
|
|
@@ -3041,7 +3045,7 @@ declare class TeamsClient {
|
|
|
3041
3045
|
/** Destroy the singleton team. Creator-only on the cloud side. */
|
|
3042
3046
|
destroyTeam(cloudUrl: string, token: string): Promise<void>;
|
|
3043
3047
|
listMembers(cloudUrl: string, token: string, teamId: string): Promise<MemberSummary[]>;
|
|
3044
|
-
invite(cloudUrl: string, token: string, teamId: string, inviteeEmail: string, expiresInHours?: number): Promise<InviteResponse>;
|
|
3048
|
+
invite(cloudUrl: string, token: string, teamId: string, inviteeEmail: string, expiresInHours?: number, inviterUserId?: string): Promise<InviteResponse>;
|
|
3045
3049
|
kickMember(cloudUrl: string, token: string, teamId: string, userId: string): Promise<void>;
|
|
3046
3050
|
me(cloudUrl: string, token: string): Promise<{
|
|
3047
3051
|
user: {
|
package/dist/index.js
CHANGED
|
@@ -5817,7 +5817,9 @@ async function probeCloud(targetUrl) {
|
|
|
5817
5817
|
// src/teams/server/auth.ts
|
|
5818
5818
|
import { createHash as createHash5, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
|
|
5819
5819
|
var TOKEN_PREFIX = "lat_";
|
|
5820
|
+
var INVITE_PREFIX = "latinv_";
|
|
5820
5821
|
var TOKEN_BYTES = 32;
|
|
5822
|
+
var INVITE_BYTES = 24;
|
|
5821
5823
|
function hashToken(rawToken) {
|
|
5822
5824
|
return createHash5("sha256").update(rawToken).digest("hex");
|
|
5823
5825
|
}
|
|
@@ -5825,6 +5827,10 @@ function generateToken() {
|
|
|
5825
5827
|
const raw = `${TOKEN_PREFIX}${randomBytes4(TOKEN_BYTES).toString("hex")}`;
|
|
5826
5828
|
return { raw, hash: hashToken(raw) };
|
|
5827
5829
|
}
|
|
5830
|
+
function generateInviteToken() {
|
|
5831
|
+
const raw = `${INVITE_PREFIX}${randomBytes4(INVITE_BYTES).toString("hex")}`;
|
|
5832
|
+
return { raw, hash: hashToken(raw) };
|
|
5833
|
+
}
|
|
5828
5834
|
|
|
5829
5835
|
// src/teams/register-direct.ts
|
|
5830
5836
|
function isPostgresUrl(url) {
|
|
@@ -5906,6 +5912,74 @@ async function registerDirectViaPostgres(cloudUrl, email, name, teamName) {
|
|
|
5906
5912
|
}
|
|
5907
5913
|
}
|
|
5908
5914
|
|
|
5915
|
+
// src/teams/direct-ops.ts
|
|
5916
|
+
async function listMembersDirect(db, teamId) {
|
|
5917
|
+
const members = await db.query("__lattice_team_members", {
|
|
5918
|
+
filters: [{ col: "team_id", op: "eq", val: teamId }]
|
|
5919
|
+
});
|
|
5920
|
+
if (members.length === 0) return [];
|
|
5921
|
+
const users = await db.query("__lattice_users", {
|
|
5922
|
+
filters: [
|
|
5923
|
+
{ col: "id", op: "in", val: members.map((m) => m.user_id) },
|
|
5924
|
+
{ col: "deleted_at", op: "isNull" }
|
|
5925
|
+
]
|
|
5926
|
+
});
|
|
5927
|
+
const userById = new Map(users.map((u) => [u.id, u]));
|
|
5928
|
+
const out = [];
|
|
5929
|
+
for (const m of members) {
|
|
5930
|
+
const u = userById.get(m.user_id);
|
|
5931
|
+
if (!u) continue;
|
|
5932
|
+
out.push({
|
|
5933
|
+
user_id: m.user_id,
|
|
5934
|
+
email: u.email,
|
|
5935
|
+
name: u.name,
|
|
5936
|
+
role: m.role,
|
|
5937
|
+
joined_at: m.joined_at
|
|
5938
|
+
});
|
|
5939
|
+
}
|
|
5940
|
+
return out;
|
|
5941
|
+
}
|
|
5942
|
+
async function inviteDirect(db, teamId, inviterUserId, inviteeEmail, expiresInHours = 7 * 24) {
|
|
5943
|
+
const team = await db.get("__lattice_team", teamId);
|
|
5944
|
+
if (!team || team.deleted_at) {
|
|
5945
|
+
throw new Error(`Team not found: ${teamId}`);
|
|
5946
|
+
}
|
|
5947
|
+
const expiresAt = new Date(Date.now() + expiresInHours * 36e5).toISOString();
|
|
5948
|
+
const { raw, hash } = generateInviteToken();
|
|
5949
|
+
const id = await db.insert("__lattice_invitations", {
|
|
5950
|
+
team_id: teamId,
|
|
5951
|
+
token_hash: hash,
|
|
5952
|
+
invitee_email: inviteeEmail,
|
|
5953
|
+
invited_by_user_id: inviterUserId,
|
|
5954
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5955
|
+
expires_at: expiresAt
|
|
5956
|
+
});
|
|
5957
|
+
return {
|
|
5958
|
+
id,
|
|
5959
|
+
raw_token: raw,
|
|
5960
|
+
expires_at: expiresAt,
|
|
5961
|
+
team_name: team.name,
|
|
5962
|
+
invitee_email: inviteeEmail
|
|
5963
|
+
};
|
|
5964
|
+
}
|
|
5965
|
+
async function kickMemberDirect(db, teamId, userId) {
|
|
5966
|
+
await db.delete("__lattice_team_members", { team_id: teamId, user_id: userId });
|
|
5967
|
+
}
|
|
5968
|
+
async function destroyTeamDirect(db) {
|
|
5969
|
+
const identityRow = await db.get("__lattice_team_identity", "singleton");
|
|
5970
|
+
if (identityRow && typeof identityRow.team_id === "string") {
|
|
5971
|
+
const teamId = identityRow.team_id;
|
|
5972
|
+
const members = await db.query("__lattice_team_members", {
|
|
5973
|
+
filters: [{ col: "team_id", op: "eq", val: teamId }]
|
|
5974
|
+
});
|
|
5975
|
+
for (const m of members) {
|
|
5976
|
+
await db.delete("__lattice_team_members", { team_id: teamId, user_id: m.user_id });
|
|
5977
|
+
}
|
|
5978
|
+
await db.update("__lattice_team", teamId, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5979
|
+
}
|
|
5980
|
+
await db.delete("__lattice_team_identity", "singleton");
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5909
5983
|
// src/teams/client.ts
|
|
5910
5984
|
var TeamsClient = class {
|
|
5911
5985
|
constructor(local) {
|
|
@@ -5992,6 +6066,13 @@ var TeamsClient = class {
|
|
|
5992
6066
|
const redeem = await this.redeemInvite(opts.cloudUrl, inviteToken, email, name);
|
|
5993
6067
|
saveDbCredential(opts.label, opts.cloudUrl);
|
|
5994
6068
|
writeToken(opts.label, redeem.raw_token);
|
|
6069
|
+
await this.saveConnection({
|
|
6070
|
+
team_id: redeem.team.id,
|
|
6071
|
+
team_name: redeem.team.name,
|
|
6072
|
+
cloud_url: opts.cloudUrl,
|
|
6073
|
+
my_user_id: redeem.user.id,
|
|
6074
|
+
api_token: redeem.raw_token
|
|
6075
|
+
});
|
|
5995
6076
|
return {
|
|
5996
6077
|
probe,
|
|
5997
6078
|
joinedAsMember: { user_id: redeem.user.id, team_id: redeem.team.id }
|
|
@@ -6011,21 +6092,51 @@ var TeamsClient = class {
|
|
|
6011
6092
|
* The HTTP path can't be used here because the browser's Fetch
|
|
6012
6093
|
* API refuses URLs with embedded credentials.
|
|
6013
6094
|
*
|
|
6014
|
-
* On success writes the bearer token to `~/.lattice/keys/<label>.token
|
|
6015
|
-
*
|
|
6016
|
-
*
|
|
6095
|
+
* On success writes the bearer token to `~/.lattice/keys/<label>.token`
|
|
6096
|
+
* **and** persists the local `__lattice_team_connections` row so the
|
|
6097
|
+
* GUI's team-management API calls can authenticate immediately
|
|
6098
|
+
* afterward (members, invites, kick, destroy). v1.13.4 added the
|
|
6099
|
+
* connection-row write — the older v1.13 implementation only wrote
|
|
6100
|
+
* the token file, leaving GUI authenticated calls with no
|
|
6101
|
+
* `cloud_url` + `my_user_id` + `api_token_encrypted` row to read.
|
|
6017
6102
|
*/
|
|
6018
6103
|
async upgradeToTeamCloud(opts) {
|
|
6019
6104
|
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);
|
|
6020
6105
|
writeToken(opts.label, reg.raw_token);
|
|
6106
|
+
await this.saveConnection({
|
|
6107
|
+
team_id: reg.team.id,
|
|
6108
|
+
team_name: reg.team.name,
|
|
6109
|
+
cloud_url: opts.cloudUrl,
|
|
6110
|
+
my_user_id: reg.user.id,
|
|
6111
|
+
api_token: reg.raw_token
|
|
6112
|
+
});
|
|
6021
6113
|
return reg;
|
|
6022
6114
|
}
|
|
6023
|
-
// ── Cloud
|
|
6115
|
+
// ── Cloud team operations (dispatch on URL scheme) ──────────────────────
|
|
6116
|
+
// For HTTP cloud URLs (`http://lattice-server:port`), every operation
|
|
6117
|
+
// round-trips through the team server's authenticated REST API. For
|
|
6118
|
+
// direct-Postgres cloud URLs (`postgres://...`), the user's `this.local`
|
|
6119
|
+
// Lattice IS the cloud DB — operations dispatch through `direct-ops.ts`
|
|
6120
|
+
// helpers that run the same INSERT / UPDATE / DELETE / SELECT logic
|
|
6121
|
+
// against `this.local` directly. The Fetch API can't handle
|
|
6122
|
+
// credentials-in-URL anyway, so the dispatch isn't optional.
|
|
6123
|
+
//
|
|
6124
|
+
// Authorization model: for HTTP clouds, the server gates by bearer
|
|
6125
|
+
// token + membership row. For direct-Postgres, possession of the
|
|
6126
|
+
// connection credential is the implicit gate — the operator is
|
|
6127
|
+
// already reading/writing the canonical data.
|
|
6024
6128
|
/** Destroy the singleton team. Creator-only on the cloud side. */
|
|
6025
6129
|
async destroyTeam(cloudUrl, token) {
|
|
6130
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6131
|
+
await destroyTeamDirect(this.local);
|
|
6132
|
+
return;
|
|
6133
|
+
}
|
|
6026
6134
|
await this.fetchAuthed(cloudUrl, token, "DELETE", "/api/team");
|
|
6027
6135
|
}
|
|
6028
6136
|
async listMembers(cloudUrl, token, teamId) {
|
|
6137
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6138
|
+
return listMembersDirect(this.local, teamId);
|
|
6139
|
+
}
|
|
6029
6140
|
const r = await this.fetchAuthed(
|
|
6030
6141
|
cloudUrl,
|
|
6031
6142
|
token,
|
|
@@ -6034,7 +6145,15 @@ var TeamsClient = class {
|
|
|
6034
6145
|
);
|
|
6035
6146
|
return r.members;
|
|
6036
6147
|
}
|
|
6037
|
-
async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours) {
|
|
6148
|
+
async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours, inviterUserId) {
|
|
6149
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6150
|
+
if (!inviterUserId) {
|
|
6151
|
+
throw new Error(
|
|
6152
|
+
"invite: inviterUserId is required for direct-Postgres cloud URLs (read it from __lattice_team_connections.my_user_id)"
|
|
6153
|
+
);
|
|
6154
|
+
}
|
|
6155
|
+
return inviteDirect(this.local, teamId, inviterUserId, inviteeEmail, expiresInHours);
|
|
6156
|
+
}
|
|
6038
6157
|
const body = { invitee_email: inviteeEmail };
|
|
6039
6158
|
if (expiresInHours !== void 0) body.expires_in_hours = expiresInHours;
|
|
6040
6159
|
return this.fetchAuthed(
|
|
@@ -6046,6 +6165,10 @@ var TeamsClient = class {
|
|
|
6046
6165
|
);
|
|
6047
6166
|
}
|
|
6048
6167
|
async kickMember(cloudUrl, token, teamId, userId) {
|
|
6168
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6169
|
+
await kickMemberDirect(this.local, teamId, userId);
|
|
6170
|
+
return;
|
|
6171
|
+
}
|
|
6049
6172
|
await this.fetchAuthed(
|
|
6050
6173
|
cloudUrl,
|
|
6051
6174
|
token,
|
|
@@ -6099,6 +6222,9 @@ var TeamsClient = class {
|
|
|
6099
6222
|
* `has_more` flag tells callers to loop until drained before sleeping.
|
|
6100
6223
|
*/
|
|
6101
6224
|
async fetchChangeBatch(cloudUrl, token, teamId, since = 0, limit = 500) {
|
|
6225
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
6226
|
+
return { envelopes: [], has_more: false };
|
|
6227
|
+
}
|
|
6102
6228
|
return this.fetchAuthed(
|
|
6103
6229
|
cloudUrl,
|
|
6104
6230
|
token,
|
package/package.json
CHANGED