@aglyn/plugins-marketing 1.0.0-beta.285 → 1.0.0-beta.288

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/plugins-marketing",
3
- "version": "1.0.0-beta.285",
3
+ "version": "1.0.0-beta.288",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,21 +25,21 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.285",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.285",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.285",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.285",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.285",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.285",
34
- "@aglyn/shared-ui-next": "1.0.0-beta.285",
35
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.285",
36
- "@aglyn/shared-util-email": "1.0.0-beta.285",
37
- "@aglyn/shared-util-http": "1.0.0-beta.285",
38
- "@aglyn/shared-util-timestamp": "1.0.0-beta.285",
39
- "@aglyn/shared-util-tools": "1.0.0-beta.285",
40
- "@aglyn/tenant-data-admin": "1.0.0-beta.285",
41
- "@aglyn/tenant-feature-instance": "1.0.0-beta.285",
42
- "@aglyn/tenant-runtime": "1.0.0-beta.285",
28
+ "@aglyn/aglyn": "1.0.0-beta.288",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.288",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.288",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.288",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.288",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.288",
34
+ "@aglyn/shared-ui-next": "1.0.0-beta.288",
35
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.288",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.288",
37
+ "@aglyn/shared-util-http": "1.0.0-beta.288",
38
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.288",
39
+ "@aglyn/shared-util-tools": "1.0.0-beta.288",
40
+ "@aglyn/tenant-data-admin": "1.0.0-beta.288",
41
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.288",
42
+ "@aglyn/tenant-runtime": "1.0.0-beta.288",
43
43
  "@swc/helpers": "0.5.23"
44
44
  },
45
45
  "peerDependencies": {
@@ -0,0 +1,25 @@
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
+ * The plugin's DECLARATIONS (AGL-4047): "Run email campaigns", which the one
19
+ * permission catalog lists beside the core keys. Loaded eagerly by the
20
+ * console (client and server) and the tenant's server through the generated
21
+ * declarations manifest, so the role editor offers it, a custom role or a
22
+ * per-member override keeps it, and the site's stamp carries it. Light by
23
+ * construction: nothing here reaches a provider, Firestore or React.
24
+ */
25
+ export declare function registerMarketingDeclarations(): void;
@@ -0,0 +1,36 @@
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 { registerPluginEntitlements } from "@aglyn/aglyn/plugin-manager/plugin-entitlements";
17
+ import { MARKETING_SITE_PERMISSIONS } from "./model/marketing-permissions.js";
18
+ let declared = false;
19
+ /**
20
+ * The plugin's DECLARATIONS (AGL-4047): "Run email campaigns", which the one
21
+ * permission catalog lists beside the core keys. Loaded eagerly by the
22
+ * console (client and server) and the tenant's server through the generated
23
+ * declarations manifest, so the role editor offers it, a custom role or a
24
+ * per-member override keeps it, and the site's stamp carries it. Light by
25
+ * construction: nothing here reaches a provider, Firestore or React.
26
+ */ export function registerMarketingDeclarations() {
27
+ if (declared) return;
28
+ declared = true;
29
+ registerPluginEntitlements({
30
+ pluginId: 'marketing',
31
+ orgPermissions: MARKETING_SITE_PERMISSIONS
32
+ });
33
+ }
34
+ registerMarketingDeclarations();
35
+
36
+ //# sourceMappingURL=declarations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/declarations.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 { registerPluginEntitlements } from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\nimport { MARKETING_SITE_PERMISSIONS } from './model/marketing-permissions'\n\nlet declared = false\n\n/**\n * The plugin's DECLARATIONS (AGL-4047): \"Run email campaigns\", which the one\n * permission catalog lists beside the core keys. Loaded eagerly by the\n * console (client and server) and the tenant's server through the generated\n * declarations manifest, so the role editor offers it, a custom role or a\n * per-member override keeps it, and the site's stamp carries it. Light by\n * construction: nothing here reaches a provider, Firestore or React.\n */\nexport function registerMarketingDeclarations(): void {\n if (declared) return\n declared = true\n registerPluginEntitlements({ pluginId: 'marketing', orgPermissions: MARKETING_SITE_PERMISSIONS })\n}\n\nregisterMarketingDeclarations()\n"],"names":["registerPluginEntitlements","MARKETING_SITE_PERMISSIONS","declared","registerMarketingDeclarations","pluginId","orgPermissions"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,0BAA0B,QAAQ,kDAAiD;AAC5F,SAASC,0BAA0B,QAAQ,mCAA+B;AAE1E,IAAIC,WAAW;AAEf;;;;;;;CAOC,GACD,OAAO,SAASC;IACd,IAAID,UAAU;IACdA,WAAW;IACXF,2BAA2B;QAAEI,UAAU;QAAaC,gBAAgBJ;IAA2B;AACjG;AAEAE"}
@@ -0,0 +1,26 @@
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
+ import type { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types';
18
+ import type { PluginOrgPermissionDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-entitlements';
19
+ /** Marketing's keys whose preset is the site role. */
20
+ export type MarketingSitePermission = 'marketing.campaigns';
21
+ /** Who ran a site's campaigns before it was a key: the site's Admin and Editor. */
22
+ export declare const MARKETING_CAMPAIGNS_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>>;
23
+ /** The key as the plugin declares it into the org permission catalog. */
24
+ export declare const MARKETING_SITE_PERMISSIONS: readonly PluginOrgPermissionDeclaration[];
25
+ /** The refusal a door answers without it: names the permission, never a role. */
26
+ export declare const MARKETING_CAMPAIGNS_REFUSAL = "Your role does not include \"Run email campaigns\" \u2014 ask an organization admin";
@@ -0,0 +1,42 @@
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 { _ as _extends } from "@swc/helpers/_/_extends";
17
+ /** Who ran a site's campaigns before it was a key: the site's Admin and Editor. */ export const MARKETING_CAMPAIGNS_SITE_ROLE_PRESET = {
18
+ admin: true,
19
+ editor: true,
20
+ author: false,
21
+ viewer: false
22
+ };
23
+ /** The key as the plugin declares it into the org permission catalog. */ export const MARKETING_SITE_PERMISSIONS = [
24
+ {
25
+ key: 'marketing.campaigns',
26
+ label: 'Run email campaigns',
27
+ description: 'Draft, send, duplicate and manage email campaigns, and see who a send reached. Follows the site role on a site, and the workspace role in the organization hub, unless a role or member sets it.',
28
+ roleDefaults: {
29
+ owner: true,
30
+ admin: true,
31
+ editor: true,
32
+ viewer: false
33
+ },
34
+ hostRoleDefaults: _extends({}, MARKETING_CAMPAIGNS_SITE_ROLE_PRESET),
35
+ group: 'marketing',
36
+ groupLabel: 'Marketing',
37
+ presetFromSiteRole: true
38
+ }
39
+ ];
40
+ /** The refusal a door answers without it: names the permission, never a role. */ export const MARKETING_CAMPAIGNS_REFUSAL = 'Your role does not include "Run email campaigns" — ask an organization admin';
41
+
42
+ //# sourceMappingURL=marketing-permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/marketing-permissions.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 type { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport type { PluginOrgPermissionDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\n\n/*\n * MARKETING'S KEY (AGL-4047).\n *\n * Composing, sending and managing email campaigns, and reading who a send\n * reached, was the site Admin's and Editor's on a site, and an owner's,\n * admin's or editor's of the whole workspace from the organization hub. That\n * is now \"Run email campaigns\", whose preset is the site role on a site\n * (`presetFromSiteRole`) and the workspace role with no site named\n * (`roleDefaults`), so every member keeps exactly the access they had, and a\n * custom role, a member's own permissions or a site's toggle can grant or\n * refuse it.\n */\n\n/** Marketing's keys whose preset is the site role. */\nexport type MarketingSitePermission = 'marketing.campaigns'\n\n/** Who ran a site's campaigns before it was a key: the site's Admin and Editor. */\nexport const MARKETING_CAMPAIGNS_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: true,\n author: false,\n viewer: false,\n}\n\n/** The key as the plugin declares it into the org permission catalog. */\nexport const MARKETING_SITE_PERMISSIONS: readonly PluginOrgPermissionDeclaration[] = [\n {\n key: 'marketing.campaigns',\n label: 'Run email campaigns',\n description:\n 'Draft, send, duplicate and manage email campaigns, and see who a send reached. Follows the site role on a site, and the workspace role in the organization hub, unless a role or member sets it.',\n roleDefaults: { owner: true, admin: true, editor: true, viewer: false },\n hostRoleDefaults: { ...MARKETING_CAMPAIGNS_SITE_ROLE_PRESET },\n group: 'marketing',\n groupLabel: 'Marketing',\n presetFromSiteRole: true,\n },\n]\n\n/** The refusal a door answers without it: names the permission, never a role. */\nexport const MARKETING_CAMPAIGNS_REFUSAL = 'Your role does not include \"Run email campaigns\" — ask an organization admin'\n"],"names":["MARKETING_CAMPAIGNS_SITE_ROLE_PRESET","admin","editor","author","viewer","MARKETING_SITE_PERMISSIONS","key","label","description","roleDefaults","owner","hostRoleDefaults","group","groupLabel","presetFromSiteRole","MARKETING_CAMPAIGNS_REFUSAL"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAqBD,iFAAiF,GACjF,OAAO,MAAMA,uCAAkF;IAC7FC,OAAO;IACPC,QAAQ;IACRC,QAAQ;IACRC,QAAQ;AACV,EAAC;AAED,uEAAuE,GACvE,OAAO,MAAMC,6BAAwE;IACnF;QACEC,KAAK;QACLC,OAAO;QACPC,aACE;QACFC,cAAc;YAAEC,OAAO;YAAMT,OAAO;YAAMC,QAAQ;YAAME,QAAQ;QAAM;QACtEO,kBAAkB,aAAKX;QACvBY,OAAO;QACPC,YAAY;QACZC,oBAAoB;IACtB;CACD,CAAA;AAED,+EAA+E,GAC/E,OAAO,MAAMC,8BAA8B,+EAA8E"}
@@ -24,10 +24,8 @@ export declare const CAMPAIGN_DRAFT_DEFAULT_NAME = "Untitled campaign";
24
24
  export declare const CAMPAIGN_DRAFT_HEADER_MAX_CHARS = 200;
25
25
  /** The most alternative subject lines or preheaders a drafted email keeps. */
26
26
  export declare const CAMPAIGN_DRAFT_MAX_VARIANTS = 10;
27
- /** The site roles that may compose this site's mail. */
28
- export declare const CAMPAIGN_DRAFT_ROLES: readonly ["admin", "editor"];
29
- /** The send route's refusal for a member who may not compose this site's mail. */
30
- export declare const CAMPAIGN_DRAFT_ROLE_REFUSAL = "Not a site admin or editor";
27
+ /** The refusal for a member without "Run email campaigns" here (AGL-4047): names the permission. */
28
+ export declare const CAMPAIGN_DRAFT_ROLE_REFUSAL = "Your role does not include \"Run email campaigns\" \u2014 ask an organization admin";
31
29
  /** The id of the one email inside a drafted campaign. */
32
30
  export declare function campaignDraftEmailId(campaignId: string): string;
33
31
  /** Every field a drafted email is written with, and so every field it can hold. */
@@ -96,7 +96,9 @@ import { claimAttempt, createResourceUid } from "@aglyn/aglyn/server";
96
96
  * are what the contact pass and the organization door need, and taking them
97
97
  * from `@aglyn/tenant-data-admin`'s index would pull the whole Next server
98
98
  * pipeline back into this file's graph.
99
- */ import { getOrgDoc, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
99
+ */ import { getOrgDoc, memberHasOrgPermission, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
100
+ import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
101
+ import { MARKETING_CAMPAIGNS_REFUSAL, MARKETING_CAMPAIGNS_SITE_ROLE_PRESET } from "../model/marketing-permissions.js";
100
102
  import { isOrgWideMember } from "@aglyn/aglyn/server";
101
103
  import { CAMPAIGN_SEND_CONTAINER_FIELD, CAMPAIGN_SEND_HOST_FIELD, campaignPlacedOnHost, campaignSiteIds, campaignVisibleTo } from "../model/campaign-container.js";
102
104
  import { campaignSendSiteStamp, orgCampaignSends, orgEmailCampaigns, sendHostId, sendIsOnHost } from "./campaign-org-refs.js";
@@ -630,11 +632,7 @@ import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
630
632
  /** A drafted campaign's name when the caller asks for none. */ export const CAMPAIGN_DRAFT_DEFAULT_NAME = 'Untitled campaign';
631
633
  /** The longest subject or preheader a drafted email stores: the send route's header cap. */ export const CAMPAIGN_DRAFT_HEADER_MAX_CHARS = 200;
632
634
  /** The most alternative subject lines or preheaders a drafted email keeps. */ export const CAMPAIGN_DRAFT_MAX_VARIANTS = 10;
633
- /** The site roles that may compose this site's mail. */ export const CAMPAIGN_DRAFT_ROLES = [
634
- 'admin',
635
- 'editor'
636
- ];
637
- /** The send route's refusal for a member who may not compose this site's mail. */ export const CAMPAIGN_DRAFT_ROLE_REFUSAL = 'Not a site admin or editor';
635
+ /** The refusal for a member without "Run email campaigns" here (AGL-4047): names the permission. */ export const CAMPAIGN_DRAFT_ROLE_REFUSAL = MARKETING_CAMPAIGNS_REFUSAL;
638
636
  /** The id of the one email inside a drafted campaign. */ export function campaignDraftEmailId(campaignId) {
639
637
  return `${campaignId}-email`;
640
638
  }
@@ -705,9 +703,9 @@ export function readCampaignDraftContent(content) {
705
703
  };
706
704
  }
707
705
  function campaignDraftRoleRefusal(host, uid) {
708
- var _host_get;
709
- const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
710
- return CAMPAIGN_DRAFT_ROLES.includes(role) ? null : {
706
+ // "Run email campaigns" (AGL-4047): the site's stamp, else the preset of
707
+ // the member's site role, the Admin and Editor this asked for before.
708
+ return holdsStampedSiteKey(host, uid, 'marketing.campaigns', MARKETING_CAMPAIGNS_SITE_ROLE_PRESET) ? null : {
711
709
  status: 403,
712
710
  error: CAMPAIGN_DRAFT_ROLE_REFUSAL
713
711
  };
@@ -890,11 +888,6 @@ export const campaignDraftWriter = createCampaignDraftWriter();
890
888
  pluginId: MARKETING_PLUGIN_ID
891
889
  });
892
890
  }
893
- /** The org roles that may manage campaigns from the organization hub. */ const ORG_MANAGE_ROLES = [
894
- 'owner',
895
- 'admin',
896
- 'editor'
897
- ];
898
891
  /**
899
892
  * The organization a `campaigns/manage` request from the organization hub is
900
893
  * for, read off its JSON body, so the dispatcher's release gate asks about the
@@ -974,7 +967,7 @@ export const campaignDraftWriter = createCampaignDraftWriter();
974
967
  const firestore = firebaseAdmin.app().firestore();
975
968
  let orgId;
976
969
  if (hostId) {
977
- var _hostSnapshot_get, _ref4;
970
+ var _ref4;
978
971
  const hostRef = firestore.collection('hosts').doc(hostId);
979
972
  const hostSnapshot = await hostRef.get();
980
973
  if (!hostSnapshot.exists) {
@@ -982,10 +975,9 @@ export const campaignDraftWriter = createCampaignDraftWriter();
982
975
  error: 'Unknown site'
983
976
  });
984
977
  }
985
- const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
986
- if (memberRole !== 'admin' && memberRole !== 'editor') {
978
+ if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketing.campaigns', MARKETING_CAMPAIGNS_SITE_ROLE_PRESET)) {
987
979
  return res.status(403).json({
988
- error: 'Not a site admin or editor'
980
+ error: MARKETING_CAMPAIGNS_REFUSAL
989
981
  });
990
982
  }
991
983
  orgId = (_ref4 = await resolveOrgIdForHost(hostId)) != null ? _ref4 : '';
@@ -1004,12 +996,15 @@ export const campaignDraftWriter = createCampaignDraftWriter();
1004
996
  });
1005
997
  }
1006
998
  } else {
1007
- var _member_role;
1008
999
  const membership = await resolveOrgMembership(decoded.uid, requestedOrgId);
1009
1000
  const member = membership == null ? void 0 : membership.member;
1010
- if (!member || !isOrgWideMember(member) || !ORG_MANAGE_ROLES.includes(String((_member_role = member.role) != null ? _member_role : ''))) {
1001
+ // A member of the whole workspace holding "Run email campaigns" with
1002
+ // no site named (AGL-4047): its default is the owner, admins and
1003
+ // editors this asked for before, then their custom role and their own
1004
+ // permissions.
1005
+ if (!member || !isOrgWideMember(member) || !await memberHasOrgPermission(requestedOrgId, member, 'marketing.campaigns')) {
1011
1006
  return res.status(403).json({
1012
- error: 'Not an organization owner, admin or editor'
1007
+ error: MARKETING_CAMPAIGNS_REFUSAL
1013
1008
  });
1014
1009
  }
1015
1010
  /*