@aglyn/tenant-data-admin 1.0.0-beta.217 → 1.0.0-beta.219
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 +9 -9
- package/src/lib/server/account-lock-mail.d.ts +74 -0
- package/src/lib/server/account-lock-mail.js +139 -0
- package/src/lib/server/account-lock-mail.js.map +1 -0
- package/src/lib/server/account-mail.d.ts +41 -0
- package/src/lib/server/account-mail.js +52 -0
- package/src/lib/server/account-mail.js.map +1 -0
- package/src/lib/server/api-v1-description.d.ts +112 -0
- package/src/lib/server/api-v1-description.js +126 -0
- package/src/lib/server/api-v1-description.js.map +1 -0
- package/src/lib/server/api-v1-kit.d.ts +156 -0
- package/src/lib/server/api-v1-kit.js +191 -0
- package/src/lib/server/api-v1-kit.js.map +1 -0
- package/src/lib/server/api-v1-resources.d.ts +144 -0
- package/src/lib/server/api-v1-resources.js +126 -0
- package/src/lib/server/api-v1-resources.js.map +1 -0
- package/src/lib/server/consent-group-change.d.ts +2 -1
- package/src/lib/server/consent-group-change.js +41 -16
- package/src/lib/server/consent-group-change.js.map +1 -1
- package/src/lib/server/contact-email-index.d.ts +22 -0
- package/src/lib/server/contact-email-index.js +108 -0
- package/src/lib/server/contact-email-index.js.map +1 -1
- package/src/lib/server/crm-email-activity.d.ts +1 -1
- package/src/lib/server/crm-email-activity.js +1 -1
- package/src/lib/server/crm-email-activity.js.map +1 -1
- package/src/lib/server/crm-inbound-email.d.ts +23 -0
- package/src/lib/server/crm-inbound-email.js +1 -16
- package/src/lib/server/crm-inbound-email.js.map +1 -1
- package/src/lib/server/crm-records.d.ts +9 -1
- package/src/lib/server/crm-records.js +19 -2
- package/src/lib/server/crm-records.js.map +1 -1
- package/src/lib/server/cron-auth.d.ts +44 -0
- package/src/lib/server/cron-auth.js +76 -0
- package/src/lib/server/cron-auth.js.map +1 -0
- package/src/lib/server/dataset-live-pages.js +6 -6
- package/src/lib/server/dataset-live-pages.js.map +1 -1
- package/src/lib/server/duplicate-resource.d.ts +26 -0
- package/src/lib/server/duplicate-resource.js +47 -25
- package/src/lib/server/duplicate-resource.js.map +1 -1
- package/src/lib/server/dynamic-list-materialize.js +4 -1
- package/src/lib/server/dynamic-list-materialize.js.map +1 -1
- package/src/lib/server/email-deliverability.js +21 -6
- package/src/lib/server/email-deliverability.js.map +1 -1
- package/src/lib/server/email-suppression.d.ts +83 -3
- package/src/lib/server/email-suppression.js +229 -3
- package/src/lib/server/email-suppression.js.map +1 -1
- package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
- package/src/lib/server/email-unsubscribe-link.js +7 -7
- package/src/lib/server/email-unsubscribe-link.js.map +1 -1
- package/src/lib/server/erase.d.ts +19 -33
- package/src/lib/server/erase.js +37 -162
- package/src/lib/server/erase.js.map +1 -1
- package/src/lib/server/host-email-tokens.js +1 -1
- package/src/lib/server/host-email-tokens.js.map +1 -1
- package/src/lib/server/host-memberships.js +3 -3
- package/src/lib/server/host-memberships.js.map +1 -1
- package/src/lib/server/lockdown.js +2 -0
- package/src/lib/server/lockdown.js.map +1 -1
- package/src/lib/server/media-download-tokens.js +2 -1
- package/src/lib/server/media-download-tokens.js.map +1 -1
- package/src/lib/server/member-email-aliases.d.ts +1 -1
- package/src/lib/server/member-email-aliases.js +1 -1
- package/src/lib/server/member-email-aliases.js.map +1 -1
- package/src/lib/server/new-account-announcement.d.ts +62 -0
- package/src/lib/server/new-account-announcement.js +77 -0
- package/src/lib/server/new-account-announcement.js.map +1 -0
- package/src/lib/server/notifications.d.ts +8 -2
- package/src/lib/server/notifications.js +18 -4
- package/src/lib/server/notifications.js.map +1 -1
- package/src/lib/server/org-member-notice.js +5 -1
- package/src/lib/server/org-member-notice.js.map +1 -1
- package/src/lib/server/organizations.js +4 -0
- package/src/lib/server/organizations.js.map +1 -1
- package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
- package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +1 -1
- package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
- package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
- package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
- package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
- package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
- package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
- package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
- package/src/lib/server/payment-provider.d.ts +173 -0
- package/src/lib/server/payment-provider.js +70 -0
- package/src/lib/server/payment-provider.js.map +1 -0
- package/src/lib/server/personal-data-export.js +1 -1
- package/src/lib/server/personal-data-export.js.map +1 -1
- package/src/lib/server/rate-limit-store.js +13 -3
- package/src/lib/server/rate-limit-store.js.map +1 -1
- package/src/lib/server/realm-plugins.js +8 -3
- package/src/lib/server/realm-plugins.js.map +1 -1
- package/src/lib/server/retained-refusals.d.ts +142 -0
- package/src/lib/server/retained-refusals.js +325 -0
- package/src/lib/server/retained-refusals.js.map +1 -0
- package/src/lib/server/risk-notice.js +21 -3
- package/src/lib/server/risk-notice.js.map +1 -1
- package/src/lib/server/serve-media-cdn.js +2 -2
- package/src/lib/server/serve-media-cdn.js.map +1 -1
- package/src/lib/server/serve-plugin-fetch.d.ts +17 -7
- package/src/lib/server/serve-plugin-fetch.js +19 -8
- package/src/lib/server/serve-plugin-fetch.js.map +1 -1
- package/src/lib/server/test-firestore-queries.d.ts +7 -0
- package/src/lib/server/test-firestore-queries.js +70 -27
- package/src/lib/server/test-firestore-queries.js.map +1 -1
- package/src/lib/server/upsert-contact.d.ts +46 -1
- package/src/lib/server/upsert-contact.js +85 -18
- package/src/lib/server/upsert-contact.js.map +1 -1
- package/src/tenancy.d.ts +4 -5
- package/src/tenancy.js +11 -6
- package/src/tenancy.js.map +1 -1
- package/src/lib/server/connect-account-status.js.map +0 -1
- package/src/lib/server/connect-payout-failure.js.map +0 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
- package/src/lib/server/contact-lifecycle-floor.js +0 -91
- package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
- package/src/lib/server/contact-merge.d.ts +0 -55
- package/src/lib/server/contact-merge.js +0 -288
- package/src/lib/server/contact-merge.js.map +0 -1
- package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
- package/src/lib/server/form-dataset-binding-token.js +0 -107
- package/src/lib/server/form-dataset-binding-token.js.map +0 -1
- package/src/lib/server/stripe-account-mode.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-data-admin",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.219",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,16 +25,16 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-util-fbserver": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.219",
|
|
29
|
+
"@aglyn/shared-util-email": "1.0.0-beta.219",
|
|
30
|
+
"@aglyn/shared-util-fbserver": "1.0.0-beta.219",
|
|
31
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.219",
|
|
32
|
+
"@aglyn/shared-util-http": "1.0.0-beta.219",
|
|
33
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.219",
|
|
34
34
|
"@msgpack/msgpack": "^3.1.3",
|
|
35
35
|
"@swc/helpers": "0.5.23",
|
|
36
|
-
"sharp": "^0.35.
|
|
37
|
-
"undici": "^
|
|
36
|
+
"sharp": "^0.35.5",
|
|
37
|
+
"undici": "^8.11.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"firebase-admin": "^14.3.0",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* WHAT AN ACCOUNT LOCK DOES TO MAIL ADDRESSED BY EMAIL (AGL-3420).
|
|
19
|
+
*
|
|
20
|
+
* The uid-keyed fan-outs already skip a locked account
|
|
21
|
+
* (`isAccountMailWithheld`, AGL-3418). What they cannot reach is mail
|
|
22
|
+
* addressed by email alone — a campaign to a list, a sequence to a contact —
|
|
23
|
+
* so the lock is written onto the suppression lists, keyed by every address
|
|
24
|
+
* the account holds:
|
|
25
|
+
*
|
|
26
|
+
* - **Every lock:** each of the house workspace's sites, `account_lock`, so
|
|
27
|
+
* Aglyn's own campaigns and sequences stop. Other workspaces are not ours
|
|
28
|
+
* to speak for.
|
|
29
|
+
* - **A ban** (`abuse`): the platform list as well, `account_ban`, which
|
|
30
|
+
* the preflight on `sendEmail` refuses on every send by any sender. The
|
|
31
|
+
* account is kept only so the address cannot sign up again; after its
|
|
32
|
+
* notice it is sent nothing.
|
|
33
|
+
*
|
|
34
|
+
* Called after the lock is durable and after its notices have gone, and it
|
|
35
|
+
* never undoes the lock: a failure is reported beside the lock, not instead
|
|
36
|
+
* of it.
|
|
37
|
+
*/
|
|
38
|
+
export interface AccountLockMailReport {
|
|
39
|
+
/** The addresses the account holds that were written or lifted. */
|
|
40
|
+
addresses: number;
|
|
41
|
+
/** A source of addresses failed; the set may be short. */
|
|
42
|
+
incomplete: boolean;
|
|
43
|
+
/** Platform ban rows filed (lock) or released (lift). */
|
|
44
|
+
banRows: number;
|
|
45
|
+
/** House sites the per-site rows were written on or lifted from. */
|
|
46
|
+
houseSites: number;
|
|
47
|
+
/** Per-site rows written (lock) or deleted (lift). */
|
|
48
|
+
houseRows: number;
|
|
49
|
+
/** Writes that threw. */
|
|
50
|
+
failed: number;
|
|
51
|
+
}
|
|
52
|
+
interface AccountLockMailInput {
|
|
53
|
+
uid: string;
|
|
54
|
+
/** The Auth record, for its addresses. */
|
|
55
|
+
record: {
|
|
56
|
+
email?: string | null;
|
|
57
|
+
providerData?: readonly {
|
|
58
|
+
email?: string | null;
|
|
59
|
+
}[];
|
|
60
|
+
} | null;
|
|
61
|
+
firestore?: any;
|
|
62
|
+
}
|
|
63
|
+
/** Write a lock onto the lists. `ban` files the platform rows as well. */
|
|
64
|
+
export declare function applyAccountLockToMail(input: AccountLockMailInput & {
|
|
65
|
+
ban: boolean;
|
|
66
|
+
}): Promise<AccountLockMailReport>;
|
|
67
|
+
/**
|
|
68
|
+
* Take a lift off the lists: the account's ban rows (restoring any
|
|
69
|
+
* suppression a ban was filed over) and its house rows.
|
|
70
|
+
*/
|
|
71
|
+
export declare function liftAccountLockFromMail(input: AccountLockMailInput & {
|
|
72
|
+
releasedByUid?: string | null;
|
|
73
|
+
}): Promise<AccountLockMailReport>;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { resolveAccountAddresses } from "./account-addresses.js";
|
|
18
|
+
import { releaseAccountBanSuppressions, releaseAccountLockSuppression, suppressEmail, suppressEmailForAccountLock } from "./email-suppression.js";
|
|
19
|
+
import { getOrgForHost } from "./organizations.js";
|
|
20
|
+
import { platformMarketingHostId } from "./platform-marketing-consent.js";
|
|
21
|
+
/** The house workspace's site ids, or none on an install without one. */ async function houseSiteIds() {
|
|
22
|
+
var _ref;
|
|
23
|
+
var _house_org;
|
|
24
|
+
const houseHostId = platformMarketingHostId();
|
|
25
|
+
if (!houseHostId) return [];
|
|
26
|
+
const house = await getOrgForHost(houseHostId);
|
|
27
|
+
const directory = (_ref = house == null ? void 0 : (_house_org = house.org) == null ? void 0 : _house_org.hosts) != null ? _ref : {};
|
|
28
|
+
const ids = Object.keys(directory).filter((id)=>Boolean(directory[id]));
|
|
29
|
+
return ids.includes(houseHostId) ? ids : [
|
|
30
|
+
houseHostId,
|
|
31
|
+
...ids
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
async function addressesOf(input) {
|
|
35
|
+
const set = await resolveAccountAddresses(_extends({
|
|
36
|
+
uid: input.uid,
|
|
37
|
+
record: input.record
|
|
38
|
+
}, input.firestore ? {
|
|
39
|
+
firestore: input.firestore
|
|
40
|
+
} : {}));
|
|
41
|
+
return {
|
|
42
|
+
emails: set.addresses.map((entry)=>entry.address),
|
|
43
|
+
incomplete: set.incomplete
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Write a lock onto the lists. `ban` files the platform rows as well. */ export async function applyAccountLockToMail(input) {
|
|
47
|
+
const { emails, incomplete } = await addressesOf(input);
|
|
48
|
+
const sites = await houseSiteIds();
|
|
49
|
+
const report = {
|
|
50
|
+
addresses: emails.length,
|
|
51
|
+
incomplete,
|
|
52
|
+
banRows: 0,
|
|
53
|
+
houseSites: sites.length,
|
|
54
|
+
houseRows: 0,
|
|
55
|
+
failed: 0
|
|
56
|
+
};
|
|
57
|
+
for (const email of emails){
|
|
58
|
+
if (input.ban) {
|
|
59
|
+
try {
|
|
60
|
+
await suppressEmail(_extends({
|
|
61
|
+
email,
|
|
62
|
+
reason: 'account_ban',
|
|
63
|
+
subjectUid: input.uid,
|
|
64
|
+
context: 'account-ban',
|
|
65
|
+
stampRecord: false
|
|
66
|
+
}, input.firestore ? {
|
|
67
|
+
firestore: input.firestore
|
|
68
|
+
} : {}));
|
|
69
|
+
report.banRows += 1;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('[account-lock-mail] ban row failed', input.uid, error);
|
|
72
|
+
report.failed += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const hostId of sites){
|
|
76
|
+
try {
|
|
77
|
+
const wrote = await suppressEmailForAccountLock(_extends({
|
|
78
|
+
hostId,
|
|
79
|
+
email,
|
|
80
|
+
uid: input.uid
|
|
81
|
+
}, input.firestore ? {
|
|
82
|
+
firestore: input.firestore
|
|
83
|
+
} : {}));
|
|
84
|
+
if (wrote) report.houseRows += 1;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('[account-lock-mail] house row failed', input.uid, hostId, error);
|
|
87
|
+
report.failed += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return report;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Take a lift off the lists: the account's ban rows (restoring any
|
|
95
|
+
* suppression a ban was filed over) and its house rows.
|
|
96
|
+
*/ export async function liftAccountLockFromMail(input) {
|
|
97
|
+
const { emails, incomplete } = await addressesOf(input);
|
|
98
|
+
const sites = await houseSiteIds();
|
|
99
|
+
const report = {
|
|
100
|
+
addresses: emails.length,
|
|
101
|
+
incomplete,
|
|
102
|
+
banRows: 0,
|
|
103
|
+
houseSites: sites.length,
|
|
104
|
+
houseRows: 0,
|
|
105
|
+
failed: 0
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
var _input_releasedByUid;
|
|
109
|
+
report.banRows = await releaseAccountBanSuppressions(_extends({
|
|
110
|
+
uid: input.uid,
|
|
111
|
+
releasedByUid: (_input_releasedByUid = input.releasedByUid) != null ? _input_releasedByUid : null
|
|
112
|
+
}, input.firestore ? {
|
|
113
|
+
firestore: input.firestore
|
|
114
|
+
} : {}));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('[account-lock-mail] ban release failed', input.uid, error);
|
|
117
|
+
report.failed += 1;
|
|
118
|
+
}
|
|
119
|
+
for (const email of emails){
|
|
120
|
+
for (const hostId of sites){
|
|
121
|
+
try {
|
|
122
|
+
const deleted = await releaseAccountLockSuppression(_extends({
|
|
123
|
+
hostId,
|
|
124
|
+
email,
|
|
125
|
+
uid: input.uid
|
|
126
|
+
}, input.firestore ? {
|
|
127
|
+
firestore: input.firestore
|
|
128
|
+
} : {}));
|
|
129
|
+
if (deleted) report.houseRows += 1;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('[account-lock-mail] house release failed', input.uid, hostId, error);
|
|
132
|
+
report.failed += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return report;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=account-lock-mail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/account-lock-mail.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolveAccountAddresses } from './account-addresses'\nimport {\n releaseAccountBanSuppressions,\n releaseAccountLockSuppression,\n suppressEmail,\n suppressEmailForAccountLock,\n} from './email-suppression'\nimport { getOrgForHost } from './organizations'\nimport { platformMarketingHostId } from './platform-marketing-consent'\n\n/**\n * WHAT AN ACCOUNT LOCK DOES TO MAIL ADDRESSED BY EMAIL (AGL-3420).\n *\n * The uid-keyed fan-outs already skip a locked account\n * (`isAccountMailWithheld`, AGL-3418). What they cannot reach is mail\n * addressed by email alone — a campaign to a list, a sequence to a contact —\n * so the lock is written onto the suppression lists, keyed by every address\n * the account holds:\n *\n * - **Every lock:** each of the house workspace's sites, `account_lock`, so\n * Aglyn's own campaigns and sequences stop. Other workspaces are not ours\n * to speak for.\n * - **A ban** (`abuse`): the platform list as well, `account_ban`, which\n * the preflight on `sendEmail` refuses on every send by any sender. The\n * account is kept only so the address cannot sign up again; after its\n * notice it is sent nothing.\n *\n * Called after the lock is durable and after its notices have gone, and it\n * never undoes the lock: a failure is reported beside the lock, not instead\n * of it.\n */\n\nexport interface AccountLockMailReport {\n /** The addresses the account holds that were written or lifted. */\n addresses: number\n /** A source of addresses failed; the set may be short. */\n incomplete: boolean\n /** Platform ban rows filed (lock) or released (lift). */\n banRows: number\n /** House sites the per-site rows were written on or lifted from. */\n houseSites: number\n /** Per-site rows written (lock) or deleted (lift). */\n houseRows: number\n /** Writes that threw. */\n failed: number\n}\n\ninterface AccountLockMailInput {\n uid: string\n /** The Auth record, for its addresses. */\n record: {\n email?: string | null\n providerData?: readonly { email?: string | null }[]\n } | null\n firestore?: any\n}\n\n/** The house workspace's site ids, or none on an install without one. */\nasync function houseSiteIds(): Promise<string[]> {\n const houseHostId = platformMarketingHostId()\n if (!houseHostId) return []\n const house = await getOrgForHost(houseHostId)\n const directory = (house?.org as { hosts?: Record<string, unknown> } | undefined)?.hosts ?? {}\n const ids = Object.keys(directory).filter((id) => Boolean(directory[id]))\n return ids.includes(houseHostId) ? ids : [houseHostId, ...ids]\n}\n\nasync function addressesOf(input: AccountLockMailInput) {\n const set = await resolveAccountAddresses({\n uid: input.uid,\n record: input.record,\n ...(input.firestore ? { firestore: input.firestore } : {}),\n })\n return { emails: set.addresses.map((entry) => entry.address), incomplete: set.incomplete }\n}\n\n/** Write a lock onto the lists. `ban` files the platform rows as well. */\nexport async function applyAccountLockToMail(\n input: AccountLockMailInput & { ban: boolean },\n): Promise<AccountLockMailReport> {\n const { emails, incomplete } = await addressesOf(input)\n const sites = await houseSiteIds()\n const report: AccountLockMailReport = {\n addresses: emails.length,\n incomplete,\n banRows: 0,\n houseSites: sites.length,\n houseRows: 0,\n failed: 0,\n }\n for (const email of emails) {\n if (input.ban) {\n try {\n await suppressEmail({\n email,\n reason: 'account_ban',\n subjectUid: input.uid,\n context: 'account-ban',\n stampRecord: false,\n ...(input.firestore ? { firestore: input.firestore } : {}),\n })\n report.banRows += 1\n } catch (error) {\n console.error('[account-lock-mail] ban row failed', input.uid, error)\n report.failed += 1\n }\n }\n for (const hostId of sites) {\n try {\n const wrote = await suppressEmailForAccountLock({\n hostId,\n email,\n uid: input.uid,\n ...(input.firestore ? { firestore: input.firestore } : {}),\n })\n if (wrote) report.houseRows += 1\n } catch (error) {\n console.error('[account-lock-mail] house row failed', input.uid, hostId, error)\n report.failed += 1\n }\n }\n }\n return report\n}\n\n/**\n * Take a lift off the lists: the account's ban rows (restoring any\n * suppression a ban was filed over) and its house rows.\n */\nexport async function liftAccountLockFromMail(\n input: AccountLockMailInput & { releasedByUid?: string | null },\n): Promise<AccountLockMailReport> {\n const { emails, incomplete } = await addressesOf(input)\n const sites = await houseSiteIds()\n const report: AccountLockMailReport = {\n addresses: emails.length,\n incomplete,\n banRows: 0,\n houseSites: sites.length,\n houseRows: 0,\n failed: 0,\n }\n try {\n report.banRows = await releaseAccountBanSuppressions({\n uid: input.uid,\n releasedByUid: input.releasedByUid ?? null,\n ...(input.firestore ? { firestore: input.firestore } : {}),\n })\n } catch (error) {\n console.error('[account-lock-mail] ban release failed', input.uid, error)\n report.failed += 1\n }\n for (const email of emails) {\n for (const hostId of sites) {\n try {\n const deleted = await releaseAccountLockSuppression({\n hostId,\n email,\n uid: input.uid,\n ...(input.firestore ? { firestore: input.firestore } : {}),\n })\n if (deleted) report.houseRows += 1\n } catch (error) {\n console.error('[account-lock-mail] house release failed', input.uid, hostId, error)\n report.failed += 1\n }\n }\n }\n return report\n}\n"],"names":["resolveAccountAddresses","releaseAccountBanSuppressions","releaseAccountLockSuppression","suppressEmail","suppressEmailForAccountLock","getOrgForHost","platformMarketingHostId","houseSiteIds","house","houseHostId","directory","org","hosts","ids","Object","keys","filter","id","Boolean","includes","addressesOf","input","set","uid","record","firestore","emails","addresses","map","entry","address","incomplete","applyAccountLockToMail","sites","report","length","banRows","houseSites","houseRows","failed","email","ban","reason","subjectUid","context","stampRecord","error","console","hostId","wrote","liftAccountLockFromMail","releasedByUid","deleted"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* WHETHER AUTOMATED PLATFORM MAIL TO ONE ACCOUNT IS WITHHELD (AGL-3418).
|
|
19
|
+
*
|
|
20
|
+
* True for an account under an active user lock (`lockdowns/user--{uid}`)
|
|
21
|
+
* or whose Auth record is disabled. Such an account cannot sign in to act on
|
|
22
|
+
* a usage alert, a digest or a notification, and for a risk lock the mail is
|
|
23
|
+
* worse than useless: it keeps a suspected bad actor informed about the
|
|
24
|
+
* workspace it was locked out of.
|
|
25
|
+
*
|
|
26
|
+
* Discretionary mail only. The lock and unlock notices, security alerts,
|
|
27
|
+
* password and verification mail, erasure confirmations and receipts do not
|
|
28
|
+
* ask this, and must not — they are how the person learns what happened.
|
|
29
|
+
*
|
|
30
|
+
* Pass `authDisabled` when the caller already holds the Auth record, to skip
|
|
31
|
+
* the pool lookup. A lookup that fails reads as "not disabled": the lock
|
|
32
|
+
* record is the authoritative signal, and an outage must not silence mail.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isAccountMailWithheld(uid: string | null | undefined, options?: {
|
|
35
|
+
authDisabled?: boolean;
|
|
36
|
+
}): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* `uids` minus every account {@link isAccountMailWithheld} withholds, in
|
|
39
|
+
* the order given. The checks run concurrently.
|
|
40
|
+
*/
|
|
41
|
+
export declare function withoutMailWithheldAccounts(uids: readonly string[]): Promise<string[]>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { isLockdownActive } from "@aglyn/aglyn/server";
|
|
17
|
+
import { findUserByUidAcrossPools } from "./auth-pools.js";
|
|
18
|
+
import { getUserLockdown } from "./lockdown.js";
|
|
19
|
+
/**
|
|
20
|
+
* WHETHER AUTOMATED PLATFORM MAIL TO ONE ACCOUNT IS WITHHELD (AGL-3418).
|
|
21
|
+
*
|
|
22
|
+
* True for an account under an active user lock (`lockdowns/user--{uid}`)
|
|
23
|
+
* or whose Auth record is disabled. Such an account cannot sign in to act on
|
|
24
|
+
* a usage alert, a digest or a notification, and for a risk lock the mail is
|
|
25
|
+
* worse than useless: it keeps a suspected bad actor informed about the
|
|
26
|
+
* workspace it was locked out of.
|
|
27
|
+
*
|
|
28
|
+
* Discretionary mail only. The lock and unlock notices, security alerts,
|
|
29
|
+
* password and verification mail, erasure confirmations and receipts do not
|
|
30
|
+
* ask this, and must not — they are how the person learns what happened.
|
|
31
|
+
*
|
|
32
|
+
* Pass `authDisabled` when the caller already holds the Auth record, to skip
|
|
33
|
+
* the pool lookup. A lookup that fails reads as "not disabled": the lock
|
|
34
|
+
* record is the authoritative signal, and an outage must not silence mail.
|
|
35
|
+
*/ export async function isAccountMailWithheld(uid, options = {}) {
|
|
36
|
+
var _pooled_record;
|
|
37
|
+
if (!uid) return false;
|
|
38
|
+
if (options.authDisabled) return true;
|
|
39
|
+
if (isLockdownActive(await getUserLockdown(uid), Date.now())) return true;
|
|
40
|
+
if (options.authDisabled === false) return false;
|
|
41
|
+
const pooled = await findUserByUidAcrossPools(uid).catch(()=>null);
|
|
42
|
+
return (pooled == null ? void 0 : (_pooled_record = pooled.record) == null ? void 0 : _pooled_record.disabled) === true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `uids` minus every account {@link isAccountMailWithheld} withholds, in
|
|
46
|
+
* the order given. The checks run concurrently.
|
|
47
|
+
*/ export async function withoutMailWithheldAccounts(uids) {
|
|
48
|
+
const withheld = await Promise.all(uids.map((uid)=>isAccountMailWithheld(uid).catch(()=>false)));
|
|
49
|
+
return uids.filter((_, index)=>!withheld[index]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=account-mail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/account-mail.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { isLockdownActive } from '@aglyn/aglyn/server'\nimport { findUserByUidAcrossPools } from './auth-pools'\nimport { getUserLockdown } from './lockdown'\n\n/**\n * WHETHER AUTOMATED PLATFORM MAIL TO ONE ACCOUNT IS WITHHELD (AGL-3418).\n *\n * True for an account under an active user lock (`lockdowns/user--{uid}`)\n * or whose Auth record is disabled. Such an account cannot sign in to act on\n * a usage alert, a digest or a notification, and for a risk lock the mail is\n * worse than useless: it keeps a suspected bad actor informed about the\n * workspace it was locked out of.\n *\n * Discretionary mail only. The lock and unlock notices, security alerts,\n * password and verification mail, erasure confirmations and receipts do not\n * ask this, and must not — they are how the person learns what happened.\n *\n * Pass `authDisabled` when the caller already holds the Auth record, to skip\n * the pool lookup. A lookup that fails reads as \"not disabled\": the lock\n * record is the authoritative signal, and an outage must not silence mail.\n */\nexport async function isAccountMailWithheld(\n uid: string | null | undefined,\n options: { authDisabled?: boolean } = {},\n): Promise<boolean> {\n if (!uid) return false\n if (options.authDisabled) return true\n if (isLockdownActive(await getUserLockdown(uid), Date.now())) return true\n if (options.authDisabled === false) return false\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n return pooled?.record?.disabled === true\n}\n\n/**\n * `uids` minus every account {@link isAccountMailWithheld} withholds, in\n * the order given. The checks run concurrently.\n */\nexport async function withoutMailWithheldAccounts(\n uids: readonly string[],\n): Promise<string[]> {\n const withheld = await Promise.all(\n uids.map((uid) => isAccountMailWithheld(uid).catch(() => false)),\n )\n return uids.filter((_, index) => !withheld[index])\n}\n"],"names":["isLockdownActive","findUserByUidAcrossPools","getUserLockdown","isAccountMailWithheld","uid","options","pooled","authDisabled","Date","now","catch","record","disabled","withoutMailWithheldAccounts","uids","withheld","Promise","all","map","filter","_","index"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,sBAAqB;AACtD,SAASC,wBAAwB,QAAQ,kBAAc;AACvD,SAASC,eAAe,QAAQ,gBAAY;AAE5C;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,eAAeC,sBACpBC,GAA8B,EAC9BC,UAAsC,CAAC,CAAC;QAOjCC;IALP,IAAI,CAACF,KAAK,OAAO;IACjB,IAAIC,QAAQE,YAAY,EAAE,OAAO;IACjC,IAAIP,iBAAiB,MAAME,gBAAgBE,MAAMI,KAAKC,GAAG,KAAK,OAAO;IACrE,IAAIJ,QAAQE,YAAY,KAAK,OAAO,OAAO;IAC3C,MAAMD,SAAS,MAAML,yBAAyBG,KAAKM,KAAK,CAAC,IAAM;IAC/D,OAAOJ,CAAAA,2BAAAA,iBAAAA,OAAQK,MAAM,qBAAdL,eAAgBM,QAAQ,MAAK;AACtC;AAEA;;;CAGC,GACD,OAAO,eAAeC,4BACpBC,IAAuB;IAEvB,MAAMC,WAAW,MAAMC,QAAQC,GAAG,CAChCH,KAAKI,GAAG,CAAC,CAACd,MAAQD,sBAAsBC,KAAKM,KAAK,CAAC,IAAM;IAE3D,OAAOI,KAAKK,MAAM,CAAC,CAACC,GAAGC,QAAU,CAACN,QAAQ,CAACM,MAAM;AACnD"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* How a `/v1` resource is described in the customer API's OpenAPI document
|
|
19
|
+
* (AGL-2733), in the shape the console's builder turns into paths, tags and
|
|
20
|
+
* schemas — whoever serves the resource.
|
|
21
|
+
*
|
|
22
|
+
* The platform describes its own resources with these; a plugin that serves
|
|
23
|
+
* one (`api-v1-resources.ts`) hands the builder its description through the
|
|
24
|
+
* registration, so the document, and the MCP tools derived from it, describe
|
|
25
|
+
* every resource the build serves and nothing it does not.
|
|
26
|
+
*
|
|
27
|
+
* Pure data and pure functions: nothing here reads a request or a store.
|
|
28
|
+
*/
|
|
29
|
+
/** A JSON Schema / OpenAPI fragment. */
|
|
30
|
+
export type ApiV1Schema = Record<string, unknown>;
|
|
31
|
+
export interface ApiV1Operation {
|
|
32
|
+
/** Path relative to the server, e.g. `/v1/contacts/{contactId}`. */
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly method: 'get' | 'post' | 'patch' | 'delete';
|
|
35
|
+
readonly operationId: string;
|
|
36
|
+
readonly summary: string;
|
|
37
|
+
readonly description?: string;
|
|
38
|
+
/** `true` when the response is the paginated list envelope. */
|
|
39
|
+
readonly list?: boolean;
|
|
40
|
+
/** Schema name for the response body, when it is a single record. */
|
|
41
|
+
readonly returns?: string;
|
|
42
|
+
/** Schema name for the request body. */
|
|
43
|
+
readonly accepts?: string;
|
|
44
|
+
/** Extra query parameters beyond the pagination pair. */
|
|
45
|
+
readonly filters?: readonly ApiV1Schema[];
|
|
46
|
+
/** Path parameters, in order. */
|
|
47
|
+
readonly pathParams?: readonly {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
}[];
|
|
51
|
+
/**
|
|
52
|
+
* `true` for a create: a fresh one answers `201`, and a replay of the same
|
|
53
|
+
* `Idempotency-Key` answers `200` with the record the original create made.
|
|
54
|
+
*/
|
|
55
|
+
readonly creates?: boolean;
|
|
56
|
+
/** Entitlement the call needs, named in the 403. */
|
|
57
|
+
readonly entitlement?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ApiV1ResourceDescription {
|
|
60
|
+
readonly tag: string;
|
|
61
|
+
readonly description: string;
|
|
62
|
+
readonly schemaName: string;
|
|
63
|
+
readonly fields: Record<string, ApiV1Schema>;
|
|
64
|
+
readonly required: readonly string[];
|
|
65
|
+
readonly ops: readonly ApiV1Operation[];
|
|
66
|
+
/**
|
|
67
|
+
* The members a write body may carry: the handler's own writable set, in the
|
|
68
|
+
* order they are described. A record returns members nothing can write —
|
|
69
|
+
* `siteId`, `sources`, the stamps — and a closed body listing them would tell
|
|
70
|
+
* a generated client to send what the handler refuses.
|
|
71
|
+
*/
|
|
72
|
+
readonly writable?: readonly string[];
|
|
73
|
+
/** Schemas for writable members the record does not return, or returns differently. */
|
|
74
|
+
readonly writeOnly?: Record<string, ApiV1Schema>;
|
|
75
|
+
/** What a create accepts that an update does not, in words. */
|
|
76
|
+
readonly writeNote?: string;
|
|
77
|
+
/** Members a write must carry. Only for a body a single method takes. */
|
|
78
|
+
readonly writeRequired?: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* Further named schemas the operations reference — a request body that is
|
|
81
|
+
* not the record's write shape (`ContactMerge`), a response that is not the
|
|
82
|
+
* record (`LeadConversion`). Each replaces a schema of the same name the
|
|
83
|
+
* builder would otherwise derive.
|
|
84
|
+
*/
|
|
85
|
+
readonly components?: Record<string, ApiV1Schema>;
|
|
86
|
+
}
|
|
87
|
+
export declare const isoField: (description: string) => ApiV1Schema;
|
|
88
|
+
export declare const stringField: (description: string) => ApiV1Schema;
|
|
89
|
+
export declare const integerField: (description: string) => ApiV1Schema;
|
|
90
|
+
export declare const booleanField: (description: string) => ApiV1Schema;
|
|
91
|
+
export declare const stringListField: (description: string) => ApiV1Schema;
|
|
92
|
+
/**
|
|
93
|
+
* A field the API may answer with `null`.
|
|
94
|
+
*
|
|
95
|
+
* OpenAPI 3.1 is JSON Schema 2020-12, where nullability is a type UNION and
|
|
96
|
+
* not the 3.0 `nullable: true` keyword — a generator reading `nullable` here
|
|
97
|
+
* would silently produce a non-optional field, which is the whole class of bug
|
|
98
|
+
* the document exists to remove.
|
|
99
|
+
*/
|
|
100
|
+
export declare const nullableField: (base: ApiV1Schema) => ApiV1Schema;
|
|
101
|
+
export declare const openObjectField: (description: string) => ApiV1Schema;
|
|
102
|
+
/** A record's `object` member, which names its kind. */
|
|
103
|
+
export declare const objectKindField: (name: string) => ApiV1Schema;
|
|
104
|
+
export declare const postalAddressField: () => ApiV1Schema;
|
|
105
|
+
/** Every record carries these two. */
|
|
106
|
+
export declare const RECORD_STAMPS: {
|
|
107
|
+
created: ApiV1Schema;
|
|
108
|
+
updated: ApiV1Schema;
|
|
109
|
+
};
|
|
110
|
+
export declare const UPDATED_AFTER_PARAM: ApiV1Schema;
|
|
111
|
+
/** An optional query parameter a list filters by. */
|
|
112
|
+
export declare const queryParam: (name: string, description: string, schema?: ApiV1Schema) => ApiV1Schema;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* How a `/v1` resource is described in the customer API's OpenAPI document
|
|
18
|
+
* (AGL-2733), in the shape the console's builder turns into paths, tags and
|
|
19
|
+
* schemas — whoever serves the resource.
|
|
20
|
+
*
|
|
21
|
+
* The platform describes its own resources with these; a plugin that serves
|
|
22
|
+
* one (`api-v1-resources.ts`) hands the builder its description through the
|
|
23
|
+
* registration, so the document, and the MCP tools derived from it, describe
|
|
24
|
+
* every resource the build serves and nothing it does not.
|
|
25
|
+
*
|
|
26
|
+
* Pure data and pure functions: nothing here reads a request or a store.
|
|
27
|
+
*/ /** A JSON Schema / OpenAPI fragment. */ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
28
|
+
// ── Field schemas ──────────────────────────────────────────────────────────
|
|
29
|
+
export const isoField = (description)=>({
|
|
30
|
+
type: 'string',
|
|
31
|
+
format: 'date-time',
|
|
32
|
+
description
|
|
33
|
+
});
|
|
34
|
+
export const stringField = (description)=>({
|
|
35
|
+
type: 'string',
|
|
36
|
+
description
|
|
37
|
+
});
|
|
38
|
+
export const integerField = (description)=>({
|
|
39
|
+
type: 'integer',
|
|
40
|
+
description
|
|
41
|
+
});
|
|
42
|
+
export const booleanField = (description)=>({
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description
|
|
45
|
+
});
|
|
46
|
+
export const stringListField = (description)=>({
|
|
47
|
+
type: 'array',
|
|
48
|
+
items: {
|
|
49
|
+
type: 'string'
|
|
50
|
+
},
|
|
51
|
+
description
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* A field the API may answer with `null`.
|
|
55
|
+
*
|
|
56
|
+
* OpenAPI 3.1 is JSON Schema 2020-12, where nullability is a type UNION and
|
|
57
|
+
* not the 3.0 `nullable: true` keyword — a generator reading `nullable` here
|
|
58
|
+
* would silently produce a non-optional field, which is the whole class of bug
|
|
59
|
+
* the document exists to remove.
|
|
60
|
+
*/ export const nullableField = (base)=>_extends({}, base, {
|
|
61
|
+
type: [
|
|
62
|
+
base['type'],
|
|
63
|
+
'null'
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
export const openObjectField = (description)=>({
|
|
67
|
+
type: 'object',
|
|
68
|
+
description,
|
|
69
|
+
additionalProperties: true
|
|
70
|
+
});
|
|
71
|
+
/** A record's `object` member, which names its kind. */ export const objectKindField = (name)=>({
|
|
72
|
+
type: 'string',
|
|
73
|
+
const: name,
|
|
74
|
+
description: `Always \`${name}\`. Lets a client discriminate a mixed array.`
|
|
75
|
+
});
|
|
76
|
+
export const postalAddressField = ()=>({
|
|
77
|
+
type: 'object',
|
|
78
|
+
description: 'Postal address. Members are optional and free-form.',
|
|
79
|
+
properties: {
|
|
80
|
+
line1: {
|
|
81
|
+
type: 'string'
|
|
82
|
+
},
|
|
83
|
+
line2: {
|
|
84
|
+
type: 'string'
|
|
85
|
+
},
|
|
86
|
+
city: {
|
|
87
|
+
type: 'string'
|
|
88
|
+
},
|
|
89
|
+
region: {
|
|
90
|
+
type: 'string'
|
|
91
|
+
},
|
|
92
|
+
postalCode: {
|
|
93
|
+
type: 'string'
|
|
94
|
+
},
|
|
95
|
+
country: {
|
|
96
|
+
type: 'string'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
additionalProperties: true
|
|
100
|
+
});
|
|
101
|
+
/** Every record carries these two. */ export const RECORD_STAMPS = {
|
|
102
|
+
created: isoField('When the record was created.'),
|
|
103
|
+
updated: isoField('When the record last changed. CRM lists can be walked by it.')
|
|
104
|
+
};
|
|
105
|
+
// ── Query parameters ───────────────────────────────────────────────────────
|
|
106
|
+
export const UPDATED_AFTER_PARAM = {
|
|
107
|
+
name: 'updatedAfter',
|
|
108
|
+
in: 'query',
|
|
109
|
+
required: false,
|
|
110
|
+
description: 'ISO 8601 instant WITH an offset (`2026-09-01T00:00:00Z`). A bare date ' + 'is a 400 — midnight in whose zone is not a question this API can ' + 'answer. Reorders the list by `updated` ascending, and moves every other ' + 'filter out of the query and onto the page, so pages can come back short.',
|
|
111
|
+
schema: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
format: 'date-time'
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/** An optional query parameter a list filters by. */ export const queryParam = (name, description, schema = {
|
|
117
|
+
type: 'string'
|
|
118
|
+
})=>({
|
|
119
|
+
name,
|
|
120
|
+
in: 'query',
|
|
121
|
+
required: false,
|
|
122
|
+
description,
|
|
123
|
+
schema
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=api-v1-description.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/api-v1-description.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How a `/v1` resource is described in the customer API's OpenAPI document\n * (AGL-2733), in the shape the console's builder turns into paths, tags and\n * schemas — whoever serves the resource.\n *\n * The platform describes its own resources with these; a plugin that serves\n * one (`api-v1-resources.ts`) hands the builder its description through the\n * registration, so the document, and the MCP tools derived from it, describe\n * every resource the build serves and nothing it does not.\n *\n * Pure data and pure functions: nothing here reads a request or a store.\n */\n\n/** A JSON Schema / OpenAPI fragment. */\nexport type ApiV1Schema = Record<string, unknown>\n\nexport interface ApiV1Operation {\n /** Path relative to the server, e.g. `/v1/contacts/{contactId}`. */\n readonly path: string\n readonly method: 'get' | 'post' | 'patch' | 'delete'\n readonly operationId: string\n readonly summary: string\n readonly description?: string\n /** `true` when the response is the paginated list envelope. */\n readonly list?: boolean\n /** Schema name for the response body, when it is a single record. */\n readonly returns?: string\n /** Schema name for the request body. */\n readonly accepts?: string\n /** Extra query parameters beyond the pagination pair. */\n readonly filters?: readonly ApiV1Schema[]\n /** Path parameters, in order. */\n readonly pathParams?: readonly { name: string; description: string }[]\n /**\n * `true` for a create: a fresh one answers `201`, and a replay of the same\n * `Idempotency-Key` answers `200` with the record the original create made.\n */\n readonly creates?: boolean\n /** Entitlement the call needs, named in the 403. */\n readonly entitlement?: string\n}\n\nexport interface ApiV1ResourceDescription {\n readonly tag: string\n readonly description: string\n readonly schemaName: string\n readonly fields: Record<string, ApiV1Schema>\n readonly required: readonly string[]\n readonly ops: readonly ApiV1Operation[]\n /**\n * The members a write body may carry: the handler's own writable set, in the\n * order they are described. A record returns members nothing can write —\n * `siteId`, `sources`, the stamps — and a closed body listing them would tell\n * a generated client to send what the handler refuses.\n */\n readonly writable?: readonly string[]\n /** Schemas for writable members the record does not return, or returns differently. */\n readonly writeOnly?: Record<string, ApiV1Schema>\n /** What a create accepts that an update does not, in words. */\n readonly writeNote?: string\n /** Members a write must carry. Only for a body a single method takes. */\n readonly writeRequired?: readonly string[]\n /**\n * Further named schemas the operations reference — a request body that is\n * not the record's write shape (`ContactMerge`), a response that is not the\n * record (`LeadConversion`). Each replaces a schema of the same name the\n * builder would otherwise derive.\n */\n readonly components?: Record<string, ApiV1Schema>\n}\n\n// ── Field schemas ──────────────────────────────────────────────────────────\n\nexport const isoField = (description: string): ApiV1Schema => ({\n type: 'string',\n format: 'date-time',\n description,\n})\n\nexport const stringField = (description: string): ApiV1Schema => ({ type: 'string', description })\nexport const integerField = (description: string): ApiV1Schema => ({ type: 'integer', description })\nexport const booleanField = (description: string): ApiV1Schema => ({ type: 'boolean', description })\nexport const stringListField = (description: string): ApiV1Schema => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * A field the API may answer with `null`.\n *\n * OpenAPI 3.1 is JSON Schema 2020-12, where nullability is a type UNION and\n * not the 3.0 `nullable: true` keyword — a generator reading `nullable` here\n * would silently produce a non-optional field, which is the whole class of bug\n * the document exists to remove.\n */\nexport const nullableField = (base: ApiV1Schema): ApiV1Schema => ({\n ...base,\n type: [base['type'], 'null'],\n})\n\nexport const openObjectField = (description: string): ApiV1Schema => ({\n type: 'object',\n description,\n additionalProperties: true,\n})\n\n/** A record's `object` member, which names its kind. */\nexport const objectKindField = (name: string): ApiV1Schema => ({\n type: 'string',\n const: name,\n description: `Always \\`${name}\\`. Lets a client discriminate a mixed array.`,\n})\n\nexport const postalAddressField = (): ApiV1Schema => ({\n type: 'object',\n description: 'Postal address. Members are optional and free-form.',\n properties: {\n line1: { type: 'string' },\n line2: { type: 'string' },\n city: { type: 'string' },\n region: { type: 'string' },\n postalCode: { type: 'string' },\n country: { type: 'string' },\n },\n additionalProperties: true,\n})\n\n/** Every record carries these two. */\nexport const RECORD_STAMPS = {\n created: isoField('When the record was created.'),\n updated: isoField('When the record last changed. CRM lists can be walked by it.'),\n}\n\n// ── Query parameters ───────────────────────────────────────────────────────\n\nexport const UPDATED_AFTER_PARAM: ApiV1Schema = {\n name: 'updatedAfter',\n in: 'query',\n required: false,\n description:\n 'ISO 8601 instant WITH an offset (`2026-09-01T00:00:00Z`). A bare date ' +\n 'is a 400 — midnight in whose zone is not a question this API can ' +\n 'answer. Reorders the list by `updated` ascending, and moves every other ' +\n 'filter out of the query and onto the page, so pages can come back short.',\n schema: { type: 'string', format: 'date-time' },\n}\n\n/** An optional query parameter a list filters by. */\nexport const queryParam = (\n name: string,\n description: string,\n schema: ApiV1Schema = { type: 'string' },\n): ApiV1Schema => ({\n name,\n in: 'query',\n required: false,\n description,\n schema,\n})\n"],"names":["isoField","description","type","format","stringField","integerField","booleanField","stringListField","items","nullableField","base","openObjectField","additionalProperties","objectKindField","name","const","postalAddressField","properties","line1","line2","city","region","postalCode","country","RECORD_STAMPS","created","updated","UPDATED_AFTER_PARAM","in","required","schema","queryParam"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,sCAAsC;AA0DtC,8EAA8E;AAE9E,OAAO,MAAMA,WAAW,CAACC,cAAsC,CAAA;QAC7DC,MAAM;QACNC,QAAQ;QACRF;IACF,CAAA,EAAE;AAEF,OAAO,MAAMG,cAAc,CAACH,cAAsC,CAAA;QAAEC,MAAM;QAAUD;IAAY,CAAA,EAAE;AAClG,OAAO,MAAMI,eAAe,CAACJ,cAAsC,CAAA;QAAEC,MAAM;QAAWD;IAAY,CAAA,EAAE;AACpG,OAAO,MAAMK,eAAe,CAACL,cAAsC,CAAA;QAAEC,MAAM;QAAWD;IAAY,CAAA,EAAE;AACpG,OAAO,MAAMM,kBAAkB,CAACN,cAAsC,CAAA;QACpEC,MAAM;QACNM,OAAO;YAAEN,MAAM;QAAS;QACxBD;IACF,CAAA,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMQ,gBAAgB,CAACC,OAAoC,aAC7DA;QACHR,MAAM;YAACQ,IAAI,CAAC,OAAO;YAAE;SAAO;OAC5B;AAEF,OAAO,MAAMC,kBAAkB,CAACV,cAAsC,CAAA;QACpEC,MAAM;QACND;QACAW,sBAAsB;IACxB,CAAA,EAAE;AAEF,sDAAsD,GACtD,OAAO,MAAMC,kBAAkB,CAACC,OAA+B,CAAA;QAC7DZ,MAAM;QACNa,OAAOD;QACPb,aAAa,CAAC,SAAS,EAAEa,KAAK,6CAA6C,CAAC;IAC9E,CAAA,EAAE;AAEF,OAAO,MAAME,qBAAqB,IAAoB,CAAA;QACpDd,MAAM;QACND,aAAa;QACbgB,YAAY;YACVC,OAAO;gBAAEhB,MAAM;YAAS;YACxBiB,OAAO;gBAAEjB,MAAM;YAAS;YACxBkB,MAAM;gBAAElB,MAAM;YAAS;YACvBmB,QAAQ;gBAAEnB,MAAM;YAAS;YACzBoB,YAAY;gBAAEpB,MAAM;YAAS;YAC7BqB,SAAS;gBAAErB,MAAM;YAAS;QAC5B;QACAU,sBAAsB;IACxB,CAAA,EAAE;AAEF,oCAAoC,GACpC,OAAO,MAAMY,gBAAgB;IAC3BC,SAASzB,SAAS;IAClB0B,SAAS1B,SAAS;AACpB,EAAC;AAED,8EAA8E;AAE9E,OAAO,MAAM2B,sBAAmC;IAC9Cb,MAAM;IACNc,IAAI;IACJC,UAAU;IACV5B,aACE,2EACA,sEACA,6EACA;IACF6B,QAAQ;QAAE5B,MAAM;QAAUC,QAAQ;IAAY;AAChD,EAAC;AAED,mDAAmD,GACnD,OAAO,MAAM4B,aAAa,CACxBjB,MACAb,aACA6B,SAAsB;IAAE5B,MAAM;AAAS,CAAC,GACvB,CAAA;QACjBY;QACAc,IAAI;QACJC,UAAU;QACV5B;QACA6B;IACF,CAAA,EAAE"}
|