@aglyn/tenant-data-admin 1.0.0-beta.160 → 1.0.0-beta.161
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
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.161",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,11 +25,11 @@
|
|
|
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-http": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.161",
|
|
29
|
+
"@aglyn/shared-util-email": "1.0.0-beta.161",
|
|
30
|
+
"@aglyn/shared-util-fbserver": "1.0.0-beta.161",
|
|
31
|
+
"@aglyn/shared-util-http": "1.0.0-beta.161",
|
|
32
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.161",
|
|
33
33
|
"@msgpack/msgpack": "^3.1.3",
|
|
34
34
|
"@swc/helpers": "0.5.23",
|
|
35
35
|
"sharp": "^0.35.3",
|
|
@@ -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
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A NOTICE TO NAMED MEMBERS OF AN ORGANIZATION, BY EMAIL (AGL-3244).
|
|
19
|
+
*
|
|
20
|
+
* The console notification (`notifications.ts`) is the record, and its email
|
|
21
|
+
* channel is a preference that defaults off. Some things cannot wait on a
|
|
22
|
+
* preference: a sequence mailbox that paused itself holds every enrollment
|
|
23
|
+
* on it until its owner acts, and the owner is not watching the page. This
|
|
24
|
+
* sends the email regardless, to the members named and — when asked — to
|
|
25
|
+
* the organization's owners and admins beside them, one send per address
|
|
26
|
+
* so nobody is shown anyone else's.
|
|
27
|
+
*
|
|
28
|
+
* Addresses come from the roster (`orgs/{orgId}/members/{uid}.email`), and
|
|
29
|
+
* for a member whose row carries none, from the directory. Suppressed
|
|
30
|
+
* addresses are skipped, the send is metered to the organization, and
|
|
31
|
+
* nothing here throws: the notice is a courtesy beside a state the caller
|
|
32
|
+
* already wrote.
|
|
33
|
+
*/
|
|
34
|
+
export interface OrgMemberNoticeInput {
|
|
35
|
+
orgId: string;
|
|
36
|
+
/** The members to write to, by uid. */
|
|
37
|
+
uids: readonly string[];
|
|
38
|
+
/** Also the organization's owners and admins. */
|
|
39
|
+
includeAdmins?: boolean;
|
|
40
|
+
subject: string;
|
|
41
|
+
text: string;
|
|
42
|
+
/** Resend tag / log label. */
|
|
43
|
+
context: string;
|
|
44
|
+
}
|
|
45
|
+
export interface OrgMemberNoticeResult {
|
|
46
|
+
/** Addresses the notice went to. */
|
|
47
|
+
sent: number;
|
|
48
|
+
/** Why nothing could be sent at all, when nothing could. */
|
|
49
|
+
reason?: 'unconfigured' | 'no-recipients' | 'failed';
|
|
50
|
+
}
|
|
51
|
+
/** Sends one notice to each member — see the module note. */
|
|
52
|
+
export declare function sendOrgMemberNotice(input: OrgMemberNoticeInput): Promise<OrgMemberNoticeResult>;
|
|
@@ -0,0 +1,90 @@
|
|
|
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 { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
|
|
17
|
+
import { findUserByUidAcrossPools } from "./auth-pools.js";
|
|
18
|
+
import { filterSuppressedEmails } from "./email-suppression.js";
|
|
19
|
+
import { listOrgMembers } from "./organizations.js";
|
|
20
|
+
/** At most this many directory lookups, and this many sends, per notice. */ const MAX_LOOKUPS = 10;
|
|
21
|
+
const MAX_SENDS = 25;
|
|
22
|
+
/** Sends one notice to each member — see the module note. */ export async function sendOrgMemberNotice(input) {
|
|
23
|
+
if (!isEmailConfigured()) return {
|
|
24
|
+
sent: 0,
|
|
25
|
+
reason: 'unconfigured'
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
const members = await listOrgMembers(input.orgId);
|
|
29
|
+
const rosterEmail = new Map();
|
|
30
|
+
for (const member of members){
|
|
31
|
+
var _member_email;
|
|
32
|
+
const address = String((_member_email = member.email) != null ? _member_email : '').trim().toLowerCase();
|
|
33
|
+
if (address.includes('@')) rosterEmail.set(member.$id, address);
|
|
34
|
+
}
|
|
35
|
+
const uids = new Set(input.uids.filter(Boolean));
|
|
36
|
+
if (input.includeAdmins) {
|
|
37
|
+
for (const member of members){
|
|
38
|
+
if (member.role === 'owner' || member.role === 'admin') uids.add(member.$id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const addresses = new Set();
|
|
42
|
+
let lookups = 0;
|
|
43
|
+
for (const uid of uids){
|
|
44
|
+
var _rosterEmail_get;
|
|
45
|
+
let address = (_rosterEmail_get = rosterEmail.get(uid)) != null ? _rosterEmail_get : '';
|
|
46
|
+
if (!address && lookups < MAX_LOOKUPS) {
|
|
47
|
+
var _ref;
|
|
48
|
+
var _pooled_record;
|
|
49
|
+
lookups += 1;
|
|
50
|
+
const pooled = await findUserByUidAcrossPools(uid).catch(()=>null);
|
|
51
|
+
address = String((_ref = pooled == null ? void 0 : (_pooled_record = pooled.record) == null ? void 0 : _pooled_record.email) != null ? _ref : '').trim().toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
if (address.includes('@')) addresses.add(address);
|
|
54
|
+
}
|
|
55
|
+
const recipients = await filterSuppressedEmails([
|
|
56
|
+
...addresses
|
|
57
|
+
]);
|
|
58
|
+
if (!recipients.length) return {
|
|
59
|
+
sent: 0,
|
|
60
|
+
reason: 'no-recipients'
|
|
61
|
+
};
|
|
62
|
+
let sent = 0;
|
|
63
|
+
for (const to of recipients.slice(0, MAX_SENDS)){
|
|
64
|
+
const result = await sendEmail({
|
|
65
|
+
to,
|
|
66
|
+
subject: input.subject,
|
|
67
|
+
text: input.text,
|
|
68
|
+
context: input.context
|
|
69
|
+
});
|
|
70
|
+
if (!result.sent) continue;
|
|
71
|
+
sent += 1;
|
|
72
|
+
const { meterOrgEmail } = await import("./email-metering.js");
|
|
73
|
+
await meterOrgEmail(input.orgId).catch(()=>undefined);
|
|
74
|
+
}
|
|
75
|
+
return sent ? {
|
|
76
|
+
sent
|
|
77
|
+
} : {
|
|
78
|
+
sent: 0,
|
|
79
|
+
reason: 'failed'
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[org-member-notice] send failed', error);
|
|
83
|
+
return {
|
|
84
|
+
sent: 0,
|
|
85
|
+
reason: 'failed'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=org-member-notice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/org-member-notice.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 { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport { findUserByUidAcrossPools } from './auth-pools'\nimport { filterSuppressedEmails } from './email-suppression'\nimport { listOrgMembers } from './organizations'\n\n/**\n * A NOTICE TO NAMED MEMBERS OF AN ORGANIZATION, BY EMAIL (AGL-3244).\n *\n * The console notification (`notifications.ts`) is the record, and its email\n * channel is a preference that defaults off. Some things cannot wait on a\n * preference: a sequence mailbox that paused itself holds every enrollment\n * on it until its owner acts, and the owner is not watching the page. This\n * sends the email regardless, to the members named and — when asked — to\n * the organization's owners and admins beside them, one send per address\n * so nobody is shown anyone else's.\n *\n * Addresses come from the roster (`orgs/{orgId}/members/{uid}.email`), and\n * for a member whose row carries none, from the directory. Suppressed\n * addresses are skipped, the send is metered to the organization, and\n * nothing here throws: the notice is a courtesy beside a state the caller\n * already wrote.\n */\n\nexport interface OrgMemberNoticeInput {\n orgId: string\n /** The members to write to, by uid. */\n uids: readonly string[]\n /** Also the organization's owners and admins. */\n includeAdmins?: boolean\n subject: string\n text: string\n /** Resend tag / log label. */\n context: string\n}\n\nexport interface OrgMemberNoticeResult {\n /** Addresses the notice went to. */\n sent: number\n /** Why nothing could be sent at all, when nothing could. */\n reason?: 'unconfigured' | 'no-recipients' | 'failed'\n}\n\n/** At most this many directory lookups, and this many sends, per notice. */\nconst MAX_LOOKUPS = 10\nconst MAX_SENDS = 25\n\n/** Sends one notice to each member — see the module note. */\nexport async function sendOrgMemberNotice(input: OrgMemberNoticeInput): Promise<OrgMemberNoticeResult> {\n if (!isEmailConfigured()) return { sent: 0, reason: 'unconfigured' }\n try {\n const members = await listOrgMembers(input.orgId)\n const rosterEmail = new Map<string, string>()\n for (const member of members) {\n const address = String(member.email ?? '')\n .trim()\n .toLowerCase()\n if (address.includes('@')) rosterEmail.set(member.$id, address)\n }\n const uids = new Set(input.uids.filter(Boolean))\n if (input.includeAdmins) {\n for (const member of members) {\n if (member.role === 'owner' || member.role === 'admin') uids.add(member.$id)\n }\n }\n const addresses = new Set<string>()\n let lookups = 0\n for (const uid of uids) {\n let address = rosterEmail.get(uid) ?? ''\n if (!address && lookups < MAX_LOOKUPS) {\n lookups += 1\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n address = String(pooled?.record?.email ?? '')\n .trim()\n .toLowerCase()\n }\n if (address.includes('@')) addresses.add(address)\n }\n const recipients = await filterSuppressedEmails([...addresses])\n if (!recipients.length) return { sent: 0, reason: 'no-recipients' }\n let sent = 0\n for (const to of recipients.slice(0, MAX_SENDS)) {\n const result = await sendEmail({ to, subject: input.subject, text: input.text, context: input.context })\n if (!result.sent) continue\n sent += 1\n const { meterOrgEmail } = await import('./email-metering')\n await meterOrgEmail(input.orgId).catch(() => undefined)\n }\n return sent ? { sent } : { sent: 0, reason: 'failed' }\n } catch (error) {\n console.error('[org-member-notice] send failed', error)\n return { sent: 0, reason: 'failed' }\n }\n}\n"],"names":["isEmailConfigured","sendEmail","findUserByUidAcrossPools","filterSuppressedEmails","listOrgMembers","MAX_LOOKUPS","MAX_SENDS","sendOrgMemberNotice","input","sent","reason","members","orgId","rosterEmail","Map","member","address","String","email","trim","toLowerCase","includes","set","$id","uids","Set","filter","Boolean","includeAdmins","role","add","addresses","lookups","uid","get","pooled","catch","record","recipients","length","to","slice","result","subject","text","context","meterOrgEmail","undefined","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,EAAEC,SAAS,QAAQ,2BAA0B;AACvE,SAASC,wBAAwB,QAAQ,kBAAc;AACvD,SAASC,sBAAsB,QAAQ,yBAAqB;AAC5D,SAASC,cAAc,QAAQ,qBAAiB;AAuChD,0EAA0E,GAC1E,MAAMC,cAAc;AACpB,MAAMC,YAAY;AAElB,2DAA2D,GAC3D,OAAO,eAAeC,oBAAoBC,KAA2B;IACnE,IAAI,CAACR,qBAAqB,OAAO;QAAES,MAAM;QAAGC,QAAQ;IAAe;IACnE,IAAI;QACF,MAAMC,UAAU,MAAMP,eAAeI,MAAMI,KAAK;QAChD,MAAMC,cAAc,IAAIC;QACxB,KAAK,MAAMC,UAAUJ,QAAS;gBACLI;YAAvB,MAAMC,UAAUC,QAAOF,gBAAAA,OAAOG,KAAK,YAAZH,gBAAgB,IACpCI,IAAI,GACJC,WAAW;YACd,IAAIJ,QAAQK,QAAQ,CAAC,MAAMR,YAAYS,GAAG,CAACP,OAAOQ,GAAG,EAAEP;QACzD;QACA,MAAMQ,OAAO,IAAIC,IAAIjB,MAAMgB,IAAI,CAACE,MAAM,CAACC;QACvC,IAAInB,MAAMoB,aAAa,EAAE;YACvB,KAAK,MAAMb,UAAUJ,QAAS;gBAC5B,IAAII,OAAOc,IAAI,KAAK,WAAWd,OAAOc,IAAI,KAAK,SAASL,KAAKM,GAAG,CAACf,OAAOQ,GAAG;YAC7E;QACF;QACA,MAAMQ,YAAY,IAAIN;QACtB,IAAIO,UAAU;QACd,KAAK,MAAMC,OAAOT,KAAM;gBACRX;YAAd,IAAIG,WAAUH,mBAAAA,YAAYqB,GAAG,CAACD,gBAAhBpB,mBAAwB;YACtC,IAAI,CAACG,WAAWgB,UAAU3B,aAAa;;oBAGpB8B;gBAFjBH,WAAW;gBACX,MAAMG,SAAS,MAAMjC,yBAAyB+B,KAAKG,KAAK,CAAC,IAAM;gBAC/DpB,UAAUC,eAAOkB,2BAAAA,iBAAAA,OAAQE,MAAM,qBAAdF,eAAgBjB,KAAK,mBAAI,IACvCC,IAAI,GACJC,WAAW;YAChB;YACA,IAAIJ,QAAQK,QAAQ,CAAC,MAAMU,UAAUD,GAAG,CAACd;QAC3C;QACA,MAAMsB,aAAa,MAAMnC,uBAAuB;eAAI4B;SAAU;QAC9D,IAAI,CAACO,WAAWC,MAAM,EAAE,OAAO;YAAE9B,MAAM;YAAGC,QAAQ;QAAgB;QAClE,IAAID,OAAO;QACX,KAAK,MAAM+B,MAAMF,WAAWG,KAAK,CAAC,GAAGnC,WAAY;YAC/C,MAAMoC,SAAS,MAAMzC,UAAU;gBAAEuC;gBAAIG,SAASnC,MAAMmC,OAAO;gBAAEC,MAAMpC,MAAMoC,IAAI;gBAAEC,SAASrC,MAAMqC,OAAO;YAAC;YACtG,IAAI,CAACH,OAAOjC,IAAI,EAAE;YAClBA,QAAQ;YACR,MAAM,EAAEqC,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC;YACvC,MAAMA,cAActC,MAAMI,KAAK,EAAEwB,KAAK,CAAC,IAAMW;QAC/C;QACA,OAAOtC,OAAO;YAAEA;QAAK,IAAI;YAAEA,MAAM;YAAGC,QAAQ;QAAS;IACvD,EAAE,OAAOsC,OAAO;QACdC,QAAQD,KAAK,CAAC,mCAAmCA;QACjD,OAAO;YAAEvC,MAAM;YAAGC,QAAQ;QAAS;IACrC;AACF"}
|