@aglyn/tenant-data-admin 1.0.0-beta.221 → 1.0.0-beta.222
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/package.json +7 -7
- package/src/lib/server/api-keys.js +3 -3
- package/src/lib/server/api-keys.js.map +1 -1
- package/src/lib/server/api-v1-resources.d.ts +44 -3
- package/src/lib/server/api-v1-resources.js +82 -5
- package/src/lib/server/api-v1-resources.js.map +1 -1
- package/src/lib/server/collection-live-pages.d.ts +2 -2
- package/src/lib/server/collection-live-pages.js +1 -1
- package/src/lib/server/collection-live-pages.js.map +1 -1
- package/src/lib/server/email-delivery-log.d.ts +17 -73
- package/src/lib/server/email-delivery-log.js +44 -222
- package/src/lib/server/email-delivery-log.js.map +1 -1
- package/src/lib/server/email-flow-gate.js +27 -38
- package/src/lib/server/email-flow-gate.js.map +1 -1
- package/src/lib/server/erase-person.d.ts +34 -54
- package/src/lib/server/erase-person.js +44 -239
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +12 -4
- package/src/lib/server/host-visitor-records.js +20 -10
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/hosted-page-review.js +46 -33
- package/src/lib/server/hosted-page-review.js.map +1 -1
- package/src/lib/server/link-reputation-review.d.ts +6 -2
- package/src/lib/server/link-reputation-review.js +61 -20
- package/src/lib/server/link-reputation-review.js.map +1 -1
- package/src/lib/server/live-page-drops.d.ts +67 -0
- package/src/lib/server/live-page-drops.js +57 -0
- package/src/lib/server/live-page-drops.js.map +1 -0
- package/src/lib/server/media-bandwidth-cap.d.ts +78 -0
- package/src/lib/server/media-bandwidth-cap.js +180 -0
- package/src/lib/server/media-bandwidth-cap.js.map +1 -0
- package/src/lib/server/media-cdn-icon.d.ts +77 -0
- package/src/lib/server/media-cdn-icon.js +140 -0
- package/src/lib/server/media-cdn-icon.js.map +1 -0
- package/src/lib/server/media-delivery-regeneration.d.ts +132 -0
- package/src/lib/server/media-delivery-regeneration.js +216 -0
- package/src/lib/server/media-delivery-regeneration.js.map +1 -0
- package/src/lib/server/media-delivery.d.ts +2 -0
- package/src/lib/server/media-delivery.js +2 -1
- package/src/lib/server/media-delivery.js.map +1 -1
- package/src/lib/server/media-serve-count.d.ts +50 -0
- package/src/lib/server/media-serve-count.js +62 -0
- package/src/lib/server/media-serve-count.js.map +1 -0
- package/src/lib/server/media-signing.d.ts +13 -2
- package/src/lib/server/media-signing.js +12 -8
- package/src/lib/server/media-signing.js.map +1 -1
- package/src/lib/server/media-tombstone.js +7 -5
- package/src/lib/server/media-tombstone.js.map +1 -1
- package/src/lib/server/media-variants.d.ts +151 -20
- package/src/lib/server/media-variants.js +204 -27
- package/src/lib/server/media-variants.js.map +1 -1
- package/src/lib/server/organizations.d.ts +102 -2
- package/src/lib/server/organizations.js +388 -58
- package/src/lib/server/organizations.js.map +1 -1
- package/src/lib/server/outbound-send-review.js +5 -4
- package/src/lib/server/outbound-send-review.js.map +1 -1
- package/src/lib/server/paged-sweeps.d.ts +51 -0
- package/src/lib/server/paged-sweeps.js +79 -0
- package/src/lib/server/paged-sweeps.js.map +1 -0
- package/src/lib/server/platform-marketing-consent.d.ts +0 -49
- package/src/lib/server/platform-marketing-consent.js +27 -11
- package/src/lib/server/platform-marketing-consent.js.map +1 -1
- package/src/lib/server/serve-media-cdn.d.ts +69 -1
- package/src/lib/server/serve-media-cdn.js +424 -70
- package/src/lib/server/serve-media-cdn.js.map +1 -1
- package/src/lib/server/site-icon-render.d.ts +68 -0
- package/src/lib/server/site-icon-render.js +157 -0
- package/src/lib/server/site-icon-render.js.map +1 -0
- package/src/lib/server/staff-seat.d.ts +50 -0
- package/src/lib/server/staff-seat.js +118 -0
- package/src/lib/server/staff-seat.js.map +1 -0
- package/src/lib/server/transfer-export.d.ts +46 -0
- package/src/lib/server/transfer-export.js +221 -0
- package/src/lib/server/transfer-export.js.map +1 -0
- package/src/lib/server/transfer-jobs.d.ts +385 -0
- package/src/lib/server/transfer-jobs.js +2368 -0
- package/src/lib/server/transfer-jobs.js.map +1 -0
- package/src/lib/server/transfer-packages.d.ts +127 -0
- package/src/lib/server/transfer-packages.js +1036 -0
- package/src/lib/server/transfer-packages.js.map +1 -0
- package/src/lib/server/upgrade-proposal.d.ts +97 -0
- package/src/lib/server/upgrade-proposal.js +312 -0
- package/src/lib/server/upgrade-proposal.js.map +1 -0
- package/src/lib/server/upsert-contact.d.ts +17 -8
- package/src/lib/server/upsert-contact.js +82 -16
- package/src/lib/server/upsert-contact.js.map +1 -1
- package/src/lib/server/web-risk.d.ts +48 -9
- package/src/lib/server/web-risk.js +281 -143
- package/src/lib/server/web-risk.js.map +1 -1
- package/src/tenancy.d.ts +3 -1
- package/src/tenancy.js +5 -1
- package/src/tenancy.js.map +1 -1
- package/src/lib/server/campaign-attribution-store.d.ts +0 -55
- package/src/lib/server/campaign-attribution-store.js +0 -95
- package/src/lib/server/campaign-attribution-store.js.map +0 -1
- package/src/lib/server/campaign-conversion-attribution.d.ts +0 -283
- package/src/lib/server/campaign-conversion-attribution.js +0 -450
- package/src/lib/server/campaign-conversion-attribution.js.map +0 -1
- package/src/lib/server/dataset-live-pages.d.ts +0 -179
- package/src/lib/server/dataset-live-pages.js +0 -421
- package/src/lib/server/dataset-live-pages.js.map +0 -1
- package/src/lib/server/email-revenue-attribution.d.ts +0 -185
- package/src/lib/server/email-revenue-attribution.js +0 -321
- package/src/lib/server/email-revenue-attribution.js.map +0 -1
|
@@ -19,7 +19,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
19
19
|
* Admin-SDK-only by design: org creation, slug reservation, membership
|
|
20
20
|
* and the projections the security rules authorize against are never
|
|
21
21
|
* client-writable (docs/MULTI_TENANT_FIRESTORE.md §8).
|
|
22
|
-
*/ import { consentGroupForHost, checkHostCollaboratorQuota, checkSeatQuota, countCollaboratorSeats, countManagerSeatsExcluding, createResourceUid, generateOrgSlug, projectMemberResolvedPermissions, resolveOrgPermissions, isOrgWideMember, isValidOrgSlug, hostPermissionKeys, orgPermissionLabel, pluginOrgPermissionKeys, projectHostMemberPermissions, projectHostMemberRoles, projectMemberScopeTokens, resolveCollaboratorHostPermissions, scopeTokensForHost } from "@aglyn/aglyn/server";
|
|
22
|
+
*/ import { consentGroupForHost, checkHostCollaboratorQuota, checkSeatQuota, countCollaboratorSeats, countManagerSeatsExcluding, createResourceUid, generateOrgSlug, projectMemberResolvedPermissions, resolveOrgPermissions, isOrgWideMember, isValidOrgSlug, hostPermissionKeys, parseOwnerHandoff, usesCustomerSeat, orgPermissionLabel, pluginOrgPermissionKeys, projectHostMemberPermissions, projectHostMemberRoles, projectMemberScopeTokens, resolveCollaboratorHostPermissions, scopeTokensForHost } from "@aglyn/aglyn/server";
|
|
23
23
|
import { ACCOUNT_ACQUISITION_FIELD, organizationAcquisition } from "@aglyn/aglyn/app-utils/account-acquisition";
|
|
24
24
|
import { activitySearchTokens } from "@aglyn/aglyn/app-utils/activity-search";
|
|
25
25
|
import { nameSearchKey, nameSearchReversed, nameSearchTokens } from "@aglyn/aglyn/app-utils/name-search";
|
|
@@ -36,6 +36,7 @@ import firebaseAdmin from "./firebase-admin.js";
|
|
|
36
36
|
import { enforceFreeWorkspaceCapInTransaction, readFreeWorkspaceCapConfig } from "./free-workspace-cap.js";
|
|
37
37
|
import { deleteMemberHostProjections, syncHostProjectionForMembers, syncMemberHostProjections } from "./host-memberships.js";
|
|
38
38
|
import { ORG_MEMBER_LIST_FIELD_NAMES, orgMemberListFields, orgMemberSearchTokens } from "./org-member-list-fields.js";
|
|
39
|
+
import { isStaffAccount } from "./staff-seat.js";
|
|
39
40
|
import { updateExisting } from "./update-existing.js";
|
|
40
41
|
import { attachWorkspaceDomain } from "./workspace-domains.js";
|
|
41
42
|
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
@@ -154,6 +155,7 @@ reservation, now = Date.now()) {
|
|
|
154
155
|
* `OrgSlugTakenError` when the slug is reserved; slug validity is the
|
|
155
156
|
* caller's job (API routes return 400 with policy copy).
|
|
156
157
|
*/ export async function createOrganization(options) {
|
|
158
|
+
var _options_ownerStaffSeat;
|
|
157
159
|
const { name, slug, ownerUid, ownerEmail, ownerDisplayName } = options;
|
|
158
160
|
const db = firestore();
|
|
159
161
|
const orgId = createResourceUid();
|
|
@@ -164,6 +166,9 @@ reservation, now = Date.now()) {
|
|
|
164
166
|
// the verdict knows the difference between "staff set no limit" and "we
|
|
165
167
|
// could not read it", and never treats the second as the first.
|
|
166
168
|
const capConfig = options.bypassFreeWorkspaceCap ? null : await readFreeWorkspaceCapConfig();
|
|
169
|
+
// Read outside the transaction for the same reason: an auth record, not a
|
|
170
|
+
// document this creation races with (AGL-3466).
|
|
171
|
+
const ownerStaffSeat = (_options_ownerStaffSeat = options.ownerStaffSeat) != null ? _options_ownerStaffSeat : await isStaffAccount(ownerUid);
|
|
167
172
|
await db.runTransaction(async (tx)=>{
|
|
168
173
|
const reservation = await tx.get(db.collection('orgSlugs').doc(slug));
|
|
169
174
|
const held = reservation.exists ? reservation.data() : undefined;
|
|
@@ -263,7 +268,10 @@ reservation, now = Date.now()) {
|
|
|
263
268
|
allHosts: true,
|
|
264
269
|
email: ownerEmail != null ? ownerEmail : null,
|
|
265
270
|
displayName: ownerDisplayName != null ? ownerDisplayName : null,
|
|
266
|
-
joinedAt: FieldValue.serverTimestamp()
|
|
271
|
+
joinedAt: FieldValue.serverTimestamp()
|
|
272
|
+
}, ownerStaffSeat ? {
|
|
273
|
+
staffSeat: true
|
|
274
|
+
} : {}, {
|
|
267
275
|
/*
|
|
268
276
|
* The rules projection, stamped AT CREATION (AGL-1038).
|
|
269
277
|
*
|
|
@@ -1070,7 +1078,8 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1070
1078
|
* rather than leaving the customer to infer it from a rejected click.
|
|
1071
1079
|
*/ async function assertCollaboratorSeats(options) {
|
|
1072
1080
|
const { orgRef, org, hostIds, self, read } = options;
|
|
1073
|
-
|
|
1081
|
+
// Platform staff take no seat (AGL-3466), so there is nothing to charge.
|
|
1082
|
+
if (!hostIds.length || self.staffSeat) return;
|
|
1074
1083
|
const entries = await readSeatEntries(orgRef, read);
|
|
1075
1084
|
for (const hostId of hostIds){
|
|
1076
1085
|
const used = countCollaboratorSeats(entries, hostId, self);
|
|
@@ -1086,7 +1095,8 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1086
1095
|
* every host and pays for it with a manager seat.
|
|
1087
1096
|
*/ function newlyScopedHosts(options) {
|
|
1088
1097
|
var _ref;
|
|
1089
|
-
const { role, allHosts, hostAccess, existing } = options;
|
|
1098
|
+
const { role, allHosts, hostAccess, existing, staffSeat } = options;
|
|
1099
|
+
if (staffSeat) return [];
|
|
1090
1100
|
if (isOrgWideMember({
|
|
1091
1101
|
role,
|
|
1092
1102
|
allHosts,
|
|
@@ -1094,7 +1104,9 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1094
1104
|
})) {
|
|
1095
1105
|
return [];
|
|
1096
1106
|
}
|
|
1097
|
-
|
|
1107
|
+
// A row stamped as staff held no seat on any of its sites, so a former
|
|
1108
|
+
// staff member keeping a site is taking that seat now, not re-writing one.
|
|
1109
|
+
const prior = usesCustomerSeat(existing) ? (_ref = existing == null ? void 0 : existing.hostAccess) != null ? _ref : {} : {};
|
|
1098
1110
|
return Object.keys(hostAccess).filter((hostId)=>!prior[hostId]);
|
|
1099
1111
|
}
|
|
1100
1112
|
/**
|
|
@@ -1129,7 +1141,7 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1129
1141
|
* transactional path.
|
|
1130
1142
|
*/ export async function collaboratorSeatRefusal(options) {
|
|
1131
1143
|
const { orgId, org, hostIds, self } = options;
|
|
1132
|
-
if (!hostIds.length) return null;
|
|
1144
|
+
if (!hostIds.length || (self == null ? void 0 : self.staffSeat)) return null;
|
|
1133
1145
|
try {
|
|
1134
1146
|
await assertCollaboratorSeats({
|
|
1135
1147
|
orgRef: firestore().collection('orgs').doc(orgId),
|
|
@@ -1207,7 +1219,8 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1207
1219
|
* may be added: the cap binds ADMISSION, never ACCESS.
|
|
1208
1220
|
*/ async function assertManagerSeats(options) {
|
|
1209
1221
|
const { orgRef, org, becomesManager, self, read } = options;
|
|
1210
|
-
|
|
1222
|
+
// Platform staff take no seat (AGL-3466).
|
|
1223
|
+
if (!becomesManager || self.staffSeat) return;
|
|
1211
1224
|
const entries = await readSeatEntries(orgRef, read);
|
|
1212
1225
|
const used = countManagerSeatsExcluding(entries, self);
|
|
1213
1226
|
const quota = checkSeatQuota(org, 'managers', used);
|
|
@@ -1230,6 +1243,7 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1230
1243
|
* and gives one up on the collaborator side; that is a real transition and is
|
|
1231
1244
|
* charged.
|
|
1232
1245
|
*/ function becomesOrgManager(options) {
|
|
1246
|
+
if (options.staffSeat) return false;
|
|
1233
1247
|
const next = isOrgWideMember({
|
|
1234
1248
|
role: options.role,
|
|
1235
1249
|
allHosts: options.allHosts,
|
|
@@ -1239,7 +1253,9 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1239
1253
|
// An ABSENT row is not a manager, and `isOrgWideMember(undefined)` is
|
|
1240
1254
|
// already false — but saying so explicitly keeps the "was it one before?"
|
|
1241
1255
|
// question readable next to the legacy shape that predates `allHosts`.
|
|
1242
|
-
|
|
1256
|
+
// A row stamped as staff held no seat either (AGL-3466): a former staff
|
|
1257
|
+
// manager whose row is rewritten is taking a customer seat now.
|
|
1258
|
+
return !options.existing || !usesCustomerSeat(options.existing) || !isOrgWideMember(options.existing);
|
|
1243
1259
|
}
|
|
1244
1260
|
/**
|
|
1245
1261
|
* Turn a manager-seat refusal into the 403 the admitting routes return, or
|
|
@@ -1272,7 +1288,7 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1272
1288
|
* access, because access is only ever granted through the transactional path.
|
|
1273
1289
|
*/ export async function managerSeatRefusal(options) {
|
|
1274
1290
|
const { orgId, org, becomesManager, self } = options;
|
|
1275
|
-
if (!becomesManager) return null;
|
|
1291
|
+
if (!becomesManager || (self == null ? void 0 : self.staffSeat)) return null;
|
|
1276
1292
|
try {
|
|
1277
1293
|
await assertManagerSeats({
|
|
1278
1294
|
orgRef: firestore().collection('orgs').doc(orgId),
|
|
@@ -1317,6 +1333,19 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1317
1333
|
status: 409
|
|
1318
1334
|
});
|
|
1319
1335
|
}
|
|
1336
|
+
/**
|
|
1337
|
+
* The `staffSeat` field of a member write (AGL-3466): stamped while the
|
|
1338
|
+
* account holds the `staff` claim, and taken off a row that still carries a
|
|
1339
|
+
* stamp once it does not, so the row converges on the claim the next time
|
|
1340
|
+
* anything writes it.
|
|
1341
|
+
*/ function staffSeatWrite(staffSeat, existing) {
|
|
1342
|
+
if (staffSeat) return {
|
|
1343
|
+
staffSeat: true
|
|
1344
|
+
};
|
|
1345
|
+
return (existing == null ? void 0 : existing.staffSeat) !== undefined ? {
|
|
1346
|
+
staffSeat: FieldValue.delete()
|
|
1347
|
+
} : {};
|
|
1348
|
+
}
|
|
1320
1349
|
/**
|
|
1321
1350
|
* Creates or updates a member transactionally with its reverse-index
|
|
1322
1351
|
* entry, then re-syncs host projections.
|
|
@@ -1372,6 +1401,10 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1372
1401
|
// refusing here is what lets the spec assert that NOTHING was written
|
|
1373
1402
|
// rather than that a throw happened somewhere.
|
|
1374
1403
|
if (role === 'owner') throw new OrgOwnerSeatError('grant');
|
|
1404
|
+
// Platform staff take none of the customer's seats (AGL-3466). Asked of
|
|
1405
|
+
// the auth record at write time, so the claim decides it rather than
|
|
1406
|
+
// anything the caller says, and a lookup that fails answers "not staff".
|
|
1407
|
+
const staffSeat = await isStaffAccount(uid);
|
|
1375
1408
|
const db = firestore();
|
|
1376
1409
|
await db.runTransaction(async (tx)=>{
|
|
1377
1410
|
var _org_name, _org_slug;
|
|
@@ -1381,6 +1414,7 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1381
1414
|
const org = orgSnapshot.data();
|
|
1382
1415
|
const memberRef = db.collection('orgs').doc(orgId).collection('members').doc(uid);
|
|
1383
1416
|
const existing = await tx.get(memberRef);
|
|
1417
|
+
const existingData = existing.data();
|
|
1384
1418
|
// The owner's own row is not writable here (AGL-1888). Both facts, not
|
|
1385
1419
|
// one standing in for the other — see the note on this function.
|
|
1386
1420
|
if (org.ownerUid === uid || ((_existing_data = existing.data()) == null ? void 0 : _existing_data.role) === 'owner') {
|
|
@@ -1400,12 +1434,14 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1400
1434
|
role,
|
|
1401
1435
|
allHosts: allHosts != null ? allHosts : false,
|
|
1402
1436
|
hostAccess: hostAccess != null ? hostAccess : {},
|
|
1403
|
-
existing:
|
|
1437
|
+
existing: existingData,
|
|
1438
|
+
staffSeat
|
|
1404
1439
|
}),
|
|
1405
1440
|
self: {
|
|
1406
1441
|
uid,
|
|
1407
1442
|
email,
|
|
1408
|
-
emails: seatAliasEmails
|
|
1443
|
+
emails: seatAliasEmails,
|
|
1444
|
+
staffSeat
|
|
1409
1445
|
},
|
|
1410
1446
|
read: (query)=>tx.get(query)
|
|
1411
1447
|
});
|
|
@@ -1422,12 +1458,14 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1422
1458
|
role,
|
|
1423
1459
|
allHosts: allHosts != null ? allHosts : false,
|
|
1424
1460
|
hostAccess: hostAccess != null ? hostAccess : {},
|
|
1425
|
-
existing:
|
|
1461
|
+
existing: existingData,
|
|
1462
|
+
staffSeat
|
|
1426
1463
|
}),
|
|
1427
1464
|
self: {
|
|
1428
1465
|
uid,
|
|
1429
1466
|
email,
|
|
1430
|
-
emails: seatAliasEmails
|
|
1467
|
+
emails: seatAliasEmails,
|
|
1468
|
+
staffSeat
|
|
1431
1469
|
},
|
|
1432
1470
|
read: (query)=>tx.get(query)
|
|
1433
1471
|
});
|
|
@@ -1435,7 +1473,7 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1435
1473
|
role,
|
|
1436
1474
|
allHosts: allHosts != null ? allHosts : false,
|
|
1437
1475
|
hostAccess: hostAccess != null ? hostAccess : {}
|
|
1438
|
-
}, roleId !== undefined ? {
|
|
1476
|
+
}, staffSeatWrite(staffSeat, existingData), roleId !== undefined ? {
|
|
1439
1477
|
roleId
|
|
1440
1478
|
} : {}, email !== undefined ? {
|
|
1441
1479
|
email
|
|
@@ -1572,6 +1610,79 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1572
1610
|
}
|
|
1573
1611
|
return written;
|
|
1574
1612
|
}
|
|
1613
|
+
/**
|
|
1614
|
+
* The writes that move the owner seat, inside the caller's transaction.
|
|
1615
|
+
*
|
|
1616
|
+
* THE ONE COPY. `transferOrgOwnership` and `acceptOwnerHandoff` both call
|
|
1617
|
+
* this, so an org can never end up with an owner one door produced and the
|
|
1618
|
+
* other would not recognize: one `ownerUid`, exactly one `role: 'owner'` row,
|
|
1619
|
+
* and reverse-index entries that agree with both.
|
|
1620
|
+
*
|
|
1621
|
+
* Every member write is a MERGE, which is what keeps a `staffSeat` stamp
|
|
1622
|
+
* where it was (AGL-3466): a staff owner handing over and staying on remains
|
|
1623
|
+
* a no-seat admin, and a staff member taking over remains a no-seat owner.
|
|
1624
|
+
*
|
|
1625
|
+
* `createdByUid` is not written here and must never be (AGL-2265) — see the
|
|
1626
|
+
* note on `transferOrgOwnership`.
|
|
1627
|
+
*/ function writeOwnershipMove(tx, db, move) {
|
|
1628
|
+
var _move_newOwnerRow, _move_newOwnerIndex;
|
|
1629
|
+
const { orgId, fromUid, toUid, previousOwner } = move;
|
|
1630
|
+
const orgRef = db.collection('orgs').doc(orgId);
|
|
1631
|
+
tx.set(orgRef, {
|
|
1632
|
+
ownerUid: toUid,
|
|
1633
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
1634
|
+
}, {
|
|
1635
|
+
merge: true
|
|
1636
|
+
});
|
|
1637
|
+
tx.set(orgRef.collection('members').doc(toUid), _extends({}, (_move_newOwnerRow = move.newOwnerRow) != null ? _move_newOwnerRow : {}, {
|
|
1638
|
+
role: 'owner',
|
|
1639
|
+
allHosts: true
|
|
1640
|
+
}), {
|
|
1641
|
+
merge: true
|
|
1642
|
+
});
|
|
1643
|
+
tx.set(db.collection('users').doc(toUid).collection('orgs').doc(orgId), // Both principals end up owner/admin, which is org-wide reach whatever
|
|
1644
|
+
// they were before — a promoted site collaborator must lose the scoped
|
|
1645
|
+
// console along with the scoped membership (AGL-1032).
|
|
1646
|
+
_extends({}, (_move_newOwnerIndex = move.newOwnerIndex) != null ? _move_newOwnerIndex : {}, {
|
|
1647
|
+
role: 'owner',
|
|
1648
|
+
orgWide: true
|
|
1649
|
+
}), {
|
|
1650
|
+
merge: true
|
|
1651
|
+
});
|
|
1652
|
+
if (previousOwner === 'stay') {
|
|
1653
|
+
tx.set(orgRef.collection('members').doc(fromUid), {
|
|
1654
|
+
role: 'admin'
|
|
1655
|
+
}, {
|
|
1656
|
+
merge: true
|
|
1657
|
+
});
|
|
1658
|
+
tx.set(db.collection('users').doc(fromUid).collection('orgs').doc(orgId), {
|
|
1659
|
+
role: 'admin',
|
|
1660
|
+
orgWide: true
|
|
1661
|
+
}, {
|
|
1662
|
+
merge: true
|
|
1663
|
+
});
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
// Leaving: the same three documents `removeOrgMember` deletes, inside this
|
|
1667
|
+
// transaction so the org is never briefly without its outgoing owner AND
|
|
1668
|
+
// without its incoming one.
|
|
1669
|
+
tx.delete(orgRef.collection('members').doc(fromUid));
|
|
1670
|
+
tx.delete(orgRef.collection(MEMBER_EMAIL_ALIASES_COLLECTION).doc(fromUid));
|
|
1671
|
+
tx.delete(db.collection('users').doc(fromUid).collection('orgs').doc(orgId));
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* The projections an ownership move changes, after its transaction: the
|
|
1675
|
+
* whole roster's auth projections, and each principal's host reverse index
|
|
1676
|
+
* (the owner spans every host — AGL-844). A principal who left has theirs
|
|
1677
|
+
* deleted, as `removeOrgMember` does.
|
|
1678
|
+
*/ async function settleOwnershipMove(move) {
|
|
1679
|
+
const { orgId, fromUid, toUid, previousOwner } = move;
|
|
1680
|
+
await syncOrgAuthProjections(orgId);
|
|
1681
|
+
await Promise.all([
|
|
1682
|
+
syncMemberHostProjections(orgId, toUid),
|
|
1683
|
+
previousOwner === 'stay' ? syncMemberHostProjections(orgId, fromUid) : deleteMemberHostProjections(orgId, fromUid)
|
|
1684
|
+
]);
|
|
1685
|
+
}
|
|
1575
1686
|
/**
|
|
1576
1687
|
* Transfers org ownership (AGL-232): the target must already be on the
|
|
1577
1688
|
* roster; the previous owner steps down to admin. One transaction across
|
|
@@ -1587,6 +1698,12 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1587
1698
|
*/ export async function transferOrgOwnership(orgId, fromUid, toUid) {
|
|
1588
1699
|
if (fromUid === toUid) throw new Error('Target already owns this org');
|
|
1589
1700
|
const db = firestore();
|
|
1701
|
+
const move = {
|
|
1702
|
+
orgId,
|
|
1703
|
+
fromUid,
|
|
1704
|
+
toUid,
|
|
1705
|
+
previousOwner: 'stay'
|
|
1706
|
+
};
|
|
1590
1707
|
await db.runTransaction(async (tx)=>{
|
|
1591
1708
|
const orgRef = db.collection('orgs').doc(orgId);
|
|
1592
1709
|
const orgSnapshot = await tx.get(orgRef);
|
|
@@ -1600,45 +1717,9 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1600
1717
|
if (!target.exists) {
|
|
1601
1718
|
throw new Error('The new owner must already be an org member');
|
|
1602
1719
|
}
|
|
1603
|
-
tx
|
|
1604
|
-
ownerUid: toUid,
|
|
1605
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
1606
|
-
}, {
|
|
1607
|
-
merge: true
|
|
1608
|
-
});
|
|
1609
|
-
tx.set(targetRef, {
|
|
1610
|
-
role: 'owner',
|
|
1611
|
-
allHosts: true
|
|
1612
|
-
}, {
|
|
1613
|
-
merge: true
|
|
1614
|
-
});
|
|
1615
|
-
tx.set(orgRef.collection('members').doc(fromUid), {
|
|
1616
|
-
role: 'admin'
|
|
1617
|
-
}, {
|
|
1618
|
-
merge: true
|
|
1619
|
-
});
|
|
1620
|
-
tx.set(db.collection('users').doc(toUid).collection('orgs').doc(orgId), // Both principals end up owner/admin, which is org-wide reach whatever
|
|
1621
|
-
// they were before — a promoted site collaborator must lose the scoped
|
|
1622
|
-
// console along with the scoped membership (AGL-1032).
|
|
1623
|
-
{
|
|
1624
|
-
role: 'owner',
|
|
1625
|
-
orgWide: true
|
|
1626
|
-
}, {
|
|
1627
|
-
merge: true
|
|
1628
|
-
});
|
|
1629
|
-
tx.set(db.collection('users').doc(fromUid).collection('orgs').doc(orgId), {
|
|
1630
|
-
role: 'admin',
|
|
1631
|
-
orgWide: true
|
|
1632
|
-
}, {
|
|
1633
|
-
merge: true
|
|
1634
|
-
});
|
|
1720
|
+
writeOwnershipMove(tx, db, move);
|
|
1635
1721
|
});
|
|
1636
|
-
await
|
|
1637
|
-
// Both principals' host access changed (owner spans every host) — AGL-844.
|
|
1638
|
-
await Promise.all([
|
|
1639
|
-
syncMemberHostProjections(orgId, toUid),
|
|
1640
|
-
syncMemberHostProjections(orgId, fromUid)
|
|
1641
|
-
]);
|
|
1722
|
+
await settleOwnershipMove(move);
|
|
1642
1723
|
/*
|
|
1643
1724
|
* A workspace changing hands is the highest-consequence thing that can
|
|
1644
1725
|
* happen to an account, and until AGL-118 it left no trace anywhere: the
|
|
@@ -1670,6 +1751,248 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1670
1751
|
name: toEmail
|
|
1671
1752
|
} : {}));
|
|
1672
1753
|
}
|
|
1754
|
+
/**
|
|
1755
|
+
* An owner handoff refused because it would need a team seat the plan does
|
|
1756
|
+
* not have (AGL-3466).
|
|
1757
|
+
*
|
|
1758
|
+
* The loophole guard. A handoff reserves no seat, because the owner seat
|
|
1759
|
+
* MOVES — but a `stay` handoff keeps the outgoing owner as an admin, so the
|
|
1760
|
+
* workspace ends with one more manager than it started with. On Free that is
|
|
1761
|
+
* a second manager on a one-seat plan, and without this check any owner could
|
|
1762
|
+
* mint free managers by handing the workspace back and forth. The refusal
|
|
1763
|
+
* names the way out: the outgoing owner leaving instead.
|
|
1764
|
+
*
|
|
1765
|
+
* Separate from {@link ManagerSeatLimitError} because the remedy is
|
|
1766
|
+
* different, and a client that branches on `code` must be able to tell.
|
|
1767
|
+
*/ export class OwnerHandoffSeatError extends Error {
|
|
1768
|
+
constructor(previousOwner, quota){
|
|
1769
|
+
super(previousOwner === 'stay' ? `Team seats full (${quota.limit}) — the current owner can't stay on ` + 'as an admin after the handoff without a seat of their own. ' + 'Choose "Leave after the handoff", or add a seat first.' : `Team seats full (${quota.limit}) — the new owner needs a team ` + 'seat, and every seat is taken. Remove a member or add a seat first.');
|
|
1770
|
+
this.name = 'OwnerHandoffSeatError';
|
|
1771
|
+
this.limit = quota.limit;
|
|
1772
|
+
this.upgradeRequired = quota.upgradeRequired;
|
|
1773
|
+
this.previousOwner = previousOwner;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* An owner handoff that can no longer be accepted as asked (AGL-3466): the
|
|
1778
|
+
* invite was answered or withdrawn in the meantime, is not a handoff, is
|
|
1779
|
+
* addressed to somebody else, or the accepter already owns the workspace.
|
|
1780
|
+
*/ export class OwnerHandoffError extends Error {
|
|
1781
|
+
constructor(reason){
|
|
1782
|
+
super(reason === 'not-pending' ? 'This handoff was already answered or withdrawn.' : reason === 'not-handoff' ? 'This invitation does not hand the workspace over.' : reason === 'not-addressed' ? 'This handoff is for a different (or unverified) email.' : 'You already own this workspace.');
|
|
1783
|
+
this.name = 'OwnerHandoffError';
|
|
1784
|
+
this.reason = reason;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Turn a handoff refusal into the response the invite route returns, or
|
|
1789
|
+
* null when the error is something else and must keep propagating.
|
|
1790
|
+
*/ export function ownerHandoffRefusalResponse(error) {
|
|
1791
|
+
if (error instanceof OwnerHandoffSeatError) {
|
|
1792
|
+
return Response.json({
|
|
1793
|
+
error: error.message,
|
|
1794
|
+
code: 'owner_handoff_seat',
|
|
1795
|
+
limit: error.limit,
|
|
1796
|
+
upgradeRequired: error.upgradeRequired,
|
|
1797
|
+
previousOwner: error.previousOwner
|
|
1798
|
+
}, {
|
|
1799
|
+
status: 403
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
if (error instanceof OwnerHandoffError) {
|
|
1803
|
+
return Response.json({
|
|
1804
|
+
error: error.message,
|
|
1805
|
+
code: 'owner_handoff',
|
|
1806
|
+
reason: error.reason
|
|
1807
|
+
}, {
|
|
1808
|
+
status: error.reason === 'not-addressed' ? 403 : 409
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Does accepting this handoff leave the workspace with one more manager
|
|
1815
|
+
* seat in use than it has now? (AGL-3466)
|
|
1816
|
+
*
|
|
1817
|
+
* The incoming owner takes the owner seat. That costs nothing when they are
|
|
1818
|
+
* staff, or already hold a manager seat here. Otherwise it is one more seat —
|
|
1819
|
+
* unless the outgoing owner LEAVES and frees one, which they only do if they
|
|
1820
|
+
* held a seat to free: a staff owner leaving frees nothing.
|
|
1821
|
+
*/ function handoffTakesManagerSeat(options) {
|
|
1822
|
+
const { previousOwner, inviteeStaff, invitee, owner } = options;
|
|
1823
|
+
const heldSeat = (row)=>!!row && usesCustomerSeat(row) && isOrgWideMember(row);
|
|
1824
|
+
if (inviteeStaff || heldSeat(invitee)) return false;
|
|
1825
|
+
return !(previousOwner === 'leave' && heldSeat(owner));
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* The seat check a handoff answers, at send and again at accept.
|
|
1829
|
+
*
|
|
1830
|
+
* Grandfathered the way every seat gate here is: only the transition is
|
|
1831
|
+
* charged, so a handoff that leaves the seat count where it was is never
|
|
1832
|
+
* refused, however far over its cap the workspace already is.
|
|
1833
|
+
*/ async function assertOwnerHandoffSeat(options) {
|
|
1834
|
+
if (!handoffTakesManagerSeat(options)) return;
|
|
1835
|
+
const entries = await readSeatEntries(options.orgRef, options.read);
|
|
1836
|
+
const used = countManagerSeatsExcluding(entries, options.self);
|
|
1837
|
+
const quota = checkSeatQuota(options.org, 'managers', used);
|
|
1838
|
+
if (!quota.allowed) throw new OwnerHandoffSeatError(options.previousOwner, quota);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* The handoff seat check, asked when the invitation is SENT (AGL-3466), so
|
|
1842
|
+
* nobody is mailed a handoff that will be refused when they click it.
|
|
1843
|
+
*
|
|
1844
|
+
* Not the enforcement — `acceptOwnerHandoff` asks again inside its
|
|
1845
|
+
* transaction, against the account that actually accepts. Here the invitee
|
|
1846
|
+
* is only an address, so their current seat is read off the roster row that
|
|
1847
|
+
* carries it.
|
|
1848
|
+
*
|
|
1849
|
+
* @returns the 403 to send, or null to proceed.
|
|
1850
|
+
*/ export async function ownerHandoffSeatRefusal(options) {
|
|
1851
|
+
const { orgId, previousOwner, inviteeStaff } = options;
|
|
1852
|
+
const email = options.email.trim().toLowerCase();
|
|
1853
|
+
const db = firestore();
|
|
1854
|
+
const orgRef = db.collection('orgs').doc(orgId);
|
|
1855
|
+
const orgSnapshot = await orgRef.get();
|
|
1856
|
+
if (!orgSnapshot.exists) return null;
|
|
1857
|
+
const org = orgSnapshot.data();
|
|
1858
|
+
const ownerSnapshot = org.ownerUid ? await orgRef.collection('members').doc(org.ownerUid).get() : null;
|
|
1859
|
+
const roster = await orgRef.collection('members').where('email', '==', email).limit(1).get();
|
|
1860
|
+
try {
|
|
1861
|
+
await assertOwnerHandoffSeat({
|
|
1862
|
+
orgRef,
|
|
1863
|
+
org,
|
|
1864
|
+
previousOwner,
|
|
1865
|
+
inviteeStaff,
|
|
1866
|
+
invitee: roster.empty ? undefined : roster.docs[0].data(),
|
|
1867
|
+
owner: (ownerSnapshot == null ? void 0 : ownerSnapshot.exists) ? ownerSnapshot.data() : undefined,
|
|
1868
|
+
self: {
|
|
1869
|
+
email
|
|
1870
|
+
},
|
|
1871
|
+
read: (query)=>query.get()
|
|
1872
|
+
});
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
const refusal = ownerHandoffRefusalResponse(error);
|
|
1875
|
+
if (refusal) return refusal;
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Accept an owner handoff (AGL-3466): the invitee becomes the owner, the
|
|
1882
|
+
* outgoing owner stays on as an admin or leaves, and the invite is marked
|
|
1883
|
+
* accepted — all in ONE transaction, with the seat check inside it.
|
|
1884
|
+
*
|
|
1885
|
+
* The move itself is {@link writeOwnershipMove}, the same writes a transfer
|
|
1886
|
+
* makes. What this adds is what an invitation needs that a transfer does not:
|
|
1887
|
+
* the incoming owner may not be on the roster yet, so their row is created
|
|
1888
|
+
* with the identity a join would give it; and the outgoing owner may leave.
|
|
1889
|
+
*
|
|
1890
|
+
* The SSO lockout check is the CALLER's, for the reason
|
|
1891
|
+
* `assessOwnershipTransferLockout` gives: it cannot live in this library
|
|
1892
|
+
* without an import cycle, and `sso-transfer-lockout-wiring.spec.ts` pins
|
|
1893
|
+
* that every caller of this asks it first.
|
|
1894
|
+
*/ export async function acceptOwnerHandoff(options) {
|
|
1895
|
+
const { orgId, inviteId, uid, email, displayName, photoURL } = options;
|
|
1896
|
+
const addresses = new Set([
|
|
1897
|
+
email,
|
|
1898
|
+
...options.emails
|
|
1899
|
+
].map((address)=>address.trim().toLowerCase()));
|
|
1900
|
+
// The accepter's claim, read at accept rather than trusted from the send.
|
|
1901
|
+
const inviteeStaff = await isStaffAccount(uid);
|
|
1902
|
+
const db = firestore();
|
|
1903
|
+
let move = null;
|
|
1904
|
+
let ownerEmail = null;
|
|
1905
|
+
await db.runTransaction(async (tx)=>{
|
|
1906
|
+
var _invite_email, _org_name, _org_slug;
|
|
1907
|
+
const orgRef = db.collection('orgs').doc(orgId);
|
|
1908
|
+
const orgSnapshot = await tx.get(orgRef);
|
|
1909
|
+
if (!orgSnapshot.exists) throw new Error(`Unknown org: ${orgId}`);
|
|
1910
|
+
const org = orgSnapshot.data();
|
|
1911
|
+
const inviteRef = orgRef.collection('invites').doc(inviteId);
|
|
1912
|
+
const inviteSnapshot = await tx.get(inviteRef);
|
|
1913
|
+
const invite = inviteSnapshot.data();
|
|
1914
|
+
if (!inviteSnapshot.exists || !invite || invite['acceptedAt']) {
|
|
1915
|
+
throw new OwnerHandoffError('not-pending');
|
|
1916
|
+
}
|
|
1917
|
+
const handoff = parseOwnerHandoff(invite.handoff);
|
|
1918
|
+
if (invite['role'] !== 'owner' || !handoff) {
|
|
1919
|
+
throw new OwnerHandoffError('not-handoff');
|
|
1920
|
+
}
|
|
1921
|
+
if (!addresses.has(String((_invite_email = invite['email']) != null ? _invite_email : '').toLowerCase())) {
|
|
1922
|
+
throw new OwnerHandoffError('not-addressed');
|
|
1923
|
+
}
|
|
1924
|
+
const fromUid = typeof org.ownerUid === 'string' ? org.ownerUid : '';
|
|
1925
|
+
if (!fromUid) throw new Error(`Org ${orgId} has no owner to hand over`);
|
|
1926
|
+
if (fromUid === uid) throw new OwnerHandoffError('already-owner');
|
|
1927
|
+
const inviteeSnapshot = await tx.get(orgRef.collection('members').doc(uid));
|
|
1928
|
+
const ownerSnapshot = await tx.get(orgRef.collection('members').doc(fromUid));
|
|
1929
|
+
const invitee = inviteeSnapshot.data();
|
|
1930
|
+
const owner = ownerSnapshot.data();
|
|
1931
|
+
ownerEmail = typeof (owner == null ? void 0 : owner.email) === 'string' ? owner.email : null;
|
|
1932
|
+
await assertOwnerHandoffSeat({
|
|
1933
|
+
orgRef,
|
|
1934
|
+
org,
|
|
1935
|
+
previousOwner: handoff.previousOwner,
|
|
1936
|
+
inviteeStaff,
|
|
1937
|
+
invitee,
|
|
1938
|
+
owner,
|
|
1939
|
+
self: {
|
|
1940
|
+
uid,
|
|
1941
|
+
email,
|
|
1942
|
+
emails: [
|
|
1943
|
+
...addresses
|
|
1944
|
+
]
|
|
1945
|
+
},
|
|
1946
|
+
read: (query)=>tx.get(query)
|
|
1947
|
+
});
|
|
1948
|
+
move = {
|
|
1949
|
+
orgId,
|
|
1950
|
+
fromUid,
|
|
1951
|
+
toUid: uid,
|
|
1952
|
+
previousOwner: handoff.previousOwner,
|
|
1953
|
+
newOwnerRow: _extends({
|
|
1954
|
+
email
|
|
1955
|
+
}, displayName !== undefined ? {
|
|
1956
|
+
displayName
|
|
1957
|
+
} : {}, photoURL !== undefined ? {
|
|
1958
|
+
photoURL
|
|
1959
|
+
} : {}, staffSeatWrite(inviteeStaff, invitee), inviteeSnapshot.exists ? {} : _extends({
|
|
1960
|
+
hostAccess: {},
|
|
1961
|
+
joinedAt: FieldValue.serverTimestamp()
|
|
1962
|
+
}, typeof invite['invitedBy'] === 'string' ? {
|
|
1963
|
+
invitedBy: invite['invitedBy']
|
|
1964
|
+
} : {})),
|
|
1965
|
+
newOwnerIndex: {
|
|
1966
|
+
orgName: (_org_name = org.name) != null ? _org_name : null,
|
|
1967
|
+
slug: (_org_slug = org.slug) != null ? _org_slug : null
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
writeOwnershipMove(tx, db, move);
|
|
1971
|
+
tx.set(inviteRef, {
|
|
1972
|
+
acceptedAt: FieldValue.serverTimestamp(),
|
|
1973
|
+
acceptedBy: uid
|
|
1974
|
+
}, {
|
|
1975
|
+
merge: true
|
|
1976
|
+
});
|
|
1977
|
+
});
|
|
1978
|
+
// Assigned inside the transaction, which has returned without throwing.
|
|
1979
|
+
const settled = move;
|
|
1980
|
+
await settleOwnershipMove(settled);
|
|
1981
|
+
// Both principals, as the transfer's row names them, written by the
|
|
1982
|
+
// person who accepted — the only party who could make this happen.
|
|
1983
|
+
await logOrgActivity(orgId, {
|
|
1984
|
+
uid,
|
|
1985
|
+
email
|
|
1986
|
+
}, settled.previousOwner === 'stay' ? `Took over the workspace; ${ownerEmail != null ? ownerEmail : 'the previous owner'} stays on as an admin` : `Took over the workspace; ${ownerEmail != null ? ownerEmail : 'the previous owner'} left it`, {
|
|
1987
|
+
type: 'member',
|
|
1988
|
+
id: uid,
|
|
1989
|
+
name: email
|
|
1990
|
+
});
|
|
1991
|
+
return {
|
|
1992
|
+
previousOwnerUid: settled.fromUid,
|
|
1993
|
+
previousOwner: settled.previousOwner
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1673
1996
|
/**
|
|
1674
1997
|
* Grants (or updates) per-host access for a uid without disturbing an
|
|
1675
1998
|
* existing membership's org role or allHosts flag (AGL-238: the host user
|
|
@@ -1677,6 +2000,8 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1677
2000
|
* just this host when the uid is not on the roster yet.
|
|
1678
2001
|
*/ export async function grantHostAccess(options) {
|
|
1679
2002
|
const { orgId, uid, hostId, role, email, displayName, invitedBy } = options;
|
|
2003
|
+
// Platform staff take no collaborator seat either (AGL-3466).
|
|
2004
|
+
const staffSeat = await isStaffAccount(uid);
|
|
1680
2005
|
const db = firestore();
|
|
1681
2006
|
await db.runTransaction(async (tx)=>{
|
|
1682
2007
|
const orgRef = db.collection('orgs').doc(orgId);
|
|
@@ -1685,6 +2010,7 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1685
2010
|
const org = orgSnapshot.data();
|
|
1686
2011
|
const memberRef = orgRef.collection('members').doc(uid);
|
|
1687
2012
|
const existing = await tx.get(memberRef);
|
|
2013
|
+
const existingData = existing.data();
|
|
1688
2014
|
// Collaborator seat cap (AGL-2068). This door DID meter, but against
|
|
1689
2015
|
// `hosts/{hostId}/members` — a display roster only its own route writes,
|
|
1690
2016
|
// so it could not see anyone admitted by invite or by `/api/orgs/members`
|
|
@@ -1700,17 +2026,21 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1700
2026
|
// precisely so it is not locked out, must not be re-classified into a
|
|
1701
2027
|
// collaborator seat by the act of writing a host key onto it.
|
|
1702
2028
|
hostIds: (()=>{
|
|
1703
|
-
var
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2029
|
+
var _existingData_hostAccess;
|
|
2030
|
+
if (existing.exists && isOrgWideMember(existingData)) return [];
|
|
2031
|
+
// A stamped row held no seat on this site, so keeping it after the
|
|
2032
|
+
// claim is gone takes one now (AGL-3466).
|
|
2033
|
+
if (usesCustomerSeat(existingData) && (existingData == null ? void 0 : (_existingData_hostAccess = existingData.hostAccess) == null ? void 0 : _existingData_hostAccess[hostId])) {
|
|
2034
|
+
return [];
|
|
2035
|
+
}
|
|
1707
2036
|
return [
|
|
1708
2037
|
hostId
|
|
1709
2038
|
];
|
|
1710
2039
|
})(),
|
|
1711
2040
|
self: {
|
|
1712
2041
|
uid,
|
|
1713
|
-
email
|
|
2042
|
+
email,
|
|
2043
|
+
staffSeat
|
|
1714
2044
|
},
|
|
1715
2045
|
read: (query)=>tx.get(query)
|
|
1716
2046
|
});
|
|
@@ -1722,7 +2052,7 @@ const MEMBER_PROJECTION_WRITE = {
|
|
|
1722
2052
|
hostAccess: {
|
|
1723
2053
|
[hostId]: role
|
|
1724
2054
|
}
|
|
1725
|
-
}, email !== undefined ? {
|
|
2055
|
+
}, staffSeatWrite(staffSeat, existingData), email !== undefined ? {
|
|
1726
2056
|
email
|
|
1727
2057
|
} : {}, displayName !== undefined ? {
|
|
1728
2058
|
displayName
|