@astram/identity 0.13.0 → 0.15.0

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.
Files changed (97) hide show
  1. package/dist/features/authz/grant-admin.service.d.ts +1 -1
  2. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  3. package/dist/features/authz/grant-admin.service.js +8 -0
  4. package/dist/features/authz/grant-ceiling.d.ts +11 -3
  5. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  6. package/dist/features/authz/grant-ceiling.js +20 -3
  7. package/dist/features/authz/record-authorization.d.ts +1 -1
  8. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  9. package/dist/features/authz/record-authorization.js +3 -2
  10. package/dist/features/company/company-hierarchy.service.d.ts +6 -0
  11. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
  12. package/dist/features/company/company-hierarchy.service.js +19 -0
  13. package/dist/features/identity-feature.module.d.ts +7 -1
  14. package/dist/features/identity-feature.module.d.ts.map +1 -1
  15. package/dist/features/identity-feature.module.js +46 -1
  16. package/dist/features/membership/membership.service.d.ts +9 -0
  17. package/dist/features/membership/membership.service.d.ts.map +1 -1
  18. package/dist/features/membership/membership.service.js +40 -0
  19. package/dist/features/membership/posting.service.d.ts +18 -0
  20. package/dist/features/membership/posting.service.d.ts.map +1 -1
  21. package/dist/features/membership/posting.service.js +85 -27
  22. package/dist/features/org-bootstrap/org-bootstrap-error.d.ts +20 -0
  23. package/dist/features/org-bootstrap/org-bootstrap-error.d.ts.map +1 -0
  24. package/dist/features/org-bootstrap/org-bootstrap-error.js +24 -0
  25. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +58 -0
  26. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -0
  27. package/dist/features/org-bootstrap/org-bootstrap-file.js +323 -0
  28. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +79 -0
  29. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -0
  30. package/dist/features/org-bootstrap/org-bootstrap-plan.js +288 -0
  31. package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts +19 -0
  32. package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts.map +1 -0
  33. package/dist/features/org-bootstrap/org-bootstrap.manifest.js +30 -0
  34. package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts +8 -0
  35. package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts.map +1 -0
  36. package/dist/features/org-bootstrap/org-bootstrap.tokens.js +10 -0
  37. package/dist/features/org-bootstrap/org-names.d.ts +12 -0
  38. package/dist/features/org-bootstrap/org-names.d.ts.map +1 -0
  39. package/dist/features/org-bootstrap/org-names.js +26 -0
  40. package/dist/features/org-bootstrap/org-state.reader.d.ts +15 -0
  41. package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -0
  42. package/dist/features/org-bootstrap/org-state.reader.js +225 -0
  43. package/dist/features/org-bootstrap/root-password-source.d.ts +9 -0
  44. package/dist/features/org-bootstrap/root-password-source.d.ts.map +1 -0
  45. package/dist/features/org-bootstrap/root-password-source.js +57 -0
  46. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +61 -0
  47. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -0
  48. package/dist/features/org-bootstrap/run-org-bootstrap.js +774 -0
  49. package/dist/features/org-bootstrap/set-password-links.d.ts +42 -0
  50. package/dist/features/org-bootstrap/set-password-links.d.ts.map +1 -0
  51. package/dist/features/org-bootstrap/set-password-links.js +76 -0
  52. package/dist/features/org-structure/org-catalog.service.d.ts +13 -0
  53. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  54. package/dist/features/org-structure/org-catalog.service.js +40 -0
  55. package/dist/features/role/role-owner-transition.d.ts +17 -0
  56. package/dist/features/role/role-owner-transition.d.ts.map +1 -0
  57. package/dist/features/role/role-owner-transition.js +23 -0
  58. package/dist/features/role/role.controller.d.ts +1 -1
  59. package/dist/features/role/role.controller.d.ts.map +1 -1
  60. package/dist/features/role/role.controller.js +6 -4
  61. package/dist/features/role/role.service.d.ts +8 -2
  62. package/dist/features/role/role.service.d.ts.map +1 -1
  63. package/dist/features/role/role.service.js +13 -3
  64. package/dist/features/user/credential-provisioner.token.d.ts +8 -0
  65. package/dist/features/user/credential-provisioner.token.d.ts.map +1 -0
  66. package/dist/features/user/credential-provisioner.token.js +10 -0
  67. package/dist/features/user/invite-message.d.ts +8 -0
  68. package/dist/features/user/invite-message.d.ts.map +1 -0
  69. package/dist/features/user/invite-message.js +23 -0
  70. package/dist/features/user/invite.config.d.ts +15 -0
  71. package/dist/features/user/invite.config.d.ts.map +1 -0
  72. package/dist/features/user/invite.config.js +55 -0
  73. package/dist/features/user/invite.service.d.ts +62 -0
  74. package/dist/features/user/invite.service.d.ts.map +1 -0
  75. package/dist/features/user/invite.service.js +254 -0
  76. package/dist/features/user/invite.tokens.d.ts +11 -0
  77. package/dist/features/user/invite.tokens.d.ts.map +1 -0
  78. package/dist/features/user/invite.tokens.js +13 -0
  79. package/dist/features/user/user.controller.d.ts +13 -6
  80. package/dist/features/user/user.controller.d.ts.map +1 -1
  81. package/dist/features/user/user.controller.js +30 -4
  82. package/dist/features/user/user.service.d.ts +51 -10
  83. package/dist/features/user/user.service.d.ts.map +1 -1
  84. package/dist/features/user/user.service.js +96 -27
  85. package/dist/index.d.ts +13 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +9 -1
  88. package/dist/internal.d.ts +2 -0
  89. package/dist/internal.d.ts.map +1 -1
  90. package/dist/internal.js +3 -1
  91. package/dist/role.object.d.ts +1 -0
  92. package/dist/role.object.d.ts.map +1 -1
  93. package/dist/role.object.js +8 -0
  94. package/dist/user.object.d.ts.map +1 -1
  95. package/dist/user.object.js +3 -0
  96. package/migrations/1710000000101-AddRoleOwnerCompany.cjs +98 -0
  97. package/package.json +4 -4
@@ -0,0 +1,42 @@
1
+ import { type ResetTokenStore } from '@astram/guard';
2
+ export declare const DEFAULT_LINK_TTL_HOURS = 72;
3
+ /** Whole hours, 1 to 168. */
4
+ export declare function validateLinkTtlHours(hours: number): number;
5
+ /**
6
+ * The reset-page base, under forgot-password's rule: outside development/test it must be set, http(s)
7
+ * and not loopback. Guard's resolver also checks the email-change base, which this run never uses, so
8
+ * that one is given a placeholder that always passes. The configured value is never echoed.
9
+ */
10
+ export declare function resolveSetPasswordBase(env: NodeJS.ProcessEnv): string;
11
+ export interface LinkTarget {
12
+ username: string;
13
+ principalId: string;
14
+ /** The user's company (audit tenant). */
15
+ tenantId: string;
16
+ /** Existing user whose earlier unused tokens must die first. */
17
+ invalidateFirst: boolean;
18
+ /** The stored (canonical) address; required when the links are emailed. */
19
+ email?: string;
20
+ }
21
+ /** Emails one link (G-010 `--email-links`); `true` when the provider accepted it. Never throws. */
22
+ export type LinkDelivery = (target: LinkTarget, link: string) => Promise<boolean>;
23
+ export interface IssueLinksInput {
24
+ store: ResetTokenStore;
25
+ targets: readonly LinkTarget[];
26
+ base: string;
27
+ ttlHours: number;
28
+ out: (line: string) => void;
29
+ /** Writes the `credential.setup_link_issued` audit row (user and expiry only). */
30
+ audit: (target: LinkTarget, expiresAtMs: number) => Promise<void>;
31
+ /** Email the links instead of printing them (G-010 R12). */
32
+ deliver?: LinkDelivery;
33
+ }
34
+ export interface IssuedLinks {
35
+ issued: number;
36
+ /** With `deliver`: links the provider accepted, and links that could not be sent (killed). */
37
+ emailed: number;
38
+ notEmailed: number;
39
+ }
40
+ /** Issue one token per target; print its link once, or email it with `deliver`. */
41
+ export declare function issueSetPasswordLinks(input: IssueLinksInput): Promise<IssuedLinks>;
42
+ //# sourceMappingURL=set-password-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set-password-links.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/set-password-links.ts"],"names":[],"mappings":"AAQA,OAAO,EAAsC,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGzF,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAGzC,6BAA6B;AAC7B,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ1D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAarE;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,mGAAmG;AACnG,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAElF,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,kFAAkF;IAClF,KAAK,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,4DAA4D;IAC5D,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,mFAAmF;AACnF,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAoCxF"}
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_LINK_TTL_HOURS = void 0;
4
+ exports.validateLinkTtlHours = validateLinkTtlHours;
5
+ exports.resolveSetPasswordBase = resolveSetPasswordBase;
6
+ exports.issueSetPasswordLinks = issueSetPasswordLinks;
7
+ /**
8
+ * One-time set-password links (G-009 R8): the same token mechanism as forgot-password (32 random bytes,
9
+ * stored only as sha256 in the reset token store, per-row expiry), printed once to the operator's
10
+ * terminal, or (G-010 R12, `--email-links`) emailed through the message dispatcher instead and never
11
+ * printed. Links carry `&purpose=invite` (G-010 R11: wording on the reset page only; redemption
12
+ * ignores it). A token or link never reaches the audit row, the report or an error.
13
+ */
14
+ const node_crypto_1 = require("node:crypto");
15
+ const guard_1 = require("@astram/guard");
16
+ const org_bootstrap_error_js_1 = require("./org-bootstrap-error.js");
17
+ exports.DEFAULT_LINK_TTL_HOURS = 72;
18
+ const HOUR_MS = 3_600_000;
19
+ /** Whole hours, 1 to 168. */
20
+ function validateLinkTtlHours(hours) {
21
+ if (!Number.isInteger(hours) || hours < 1 || hours > 168) {
22
+ throw new org_bootstrap_error_js_1.OrgBootstrapError('invalid_link_ttl', 'The link lifetime must be 1 to 168 whole hours.');
23
+ }
24
+ return hours;
25
+ }
26
+ /**
27
+ * The reset-page base, under forgot-password's rule: outside development/test it must be set, http(s)
28
+ * and not loopback. Guard's resolver also checks the email-change base, which this run never uses, so
29
+ * that one is given a placeholder that always passes. The configured value is never echoed.
30
+ */
31
+ function resolveSetPasswordBase(env) {
32
+ try {
33
+ return (0, guard_1.resolveAuthLinkBaseUrls)({
34
+ env,
35
+ emailChangeLinkBaseUrlOption: 'https://email-change.not-used.example/',
36
+ mailDisabled: false,
37
+ }).resetLinkBaseUrl;
38
+ }
39
+ catch {
40
+ throw new org_bootstrap_error_js_1.OrgBootstrapError('link_base_url', 'RESET_LINK_BASE_URL must be set to a public http(s) URL (not localhost) outside development/test.');
41
+ }
42
+ }
43
+ /** Issue one token per target; print its link once, or email it with `deliver`. */
44
+ async function issueSetPasswordLinks(input) {
45
+ let issued = 0;
46
+ let emailed = 0;
47
+ let notEmailed = 0;
48
+ for (const target of input.targets) {
49
+ if (target.invalidateFirst)
50
+ await input.store.invalidateAllForPrincipal(target.principalId);
51
+ const raw = (0, node_crypto_1.randomBytes)(32).toString('hex');
52
+ const expiresAtMs = Date.now() + input.ttlHours * HOUR_MS;
53
+ await input.store.issue(target.principalId, (0, node_crypto_1.createHash)('sha256').update(raw).digest('hex'), expiresAtMs);
54
+ await input.audit(target, expiresAtMs);
55
+ issued += 1;
56
+ const link = `${input.base}?token=${raw}&purpose=invite`;
57
+ const expires = new Date(expiresAtMs).toISOString();
58
+ if (input.deliver === undefined) {
59
+ input.out(`${target.username} ${link} (expires ${expires})`);
60
+ continue;
61
+ }
62
+ const masked = (0, guard_1.maskEmail)(target.email ?? '');
63
+ if (target.email !== undefined && target.email !== '' && (await input.deliver(target, link))) {
64
+ emailed += 1;
65
+ input.out(`${target.username} set-password link emailed to ${masked} (expires ${expires})`);
66
+ }
67
+ else {
68
+ // Nobody holds this link: kill it; the operator re-runs with --reissue-links.
69
+ await input.store.invalidateAllForPrincipal(target.principalId);
70
+ notEmailed += 1;
71
+ input.out(`${target.username} set-password link could not be emailed to ${masked}; nothing printed. ` +
72
+ 'Re-run with --reissue-links to send a fresh one.');
73
+ }
74
+ }
75
+ return { issued, emailed, notEmailed };
76
+ }
@@ -13,6 +13,19 @@ export declare class OrgCatalogService {
13
13
  listCompanyDepartments(companyUuid: string): Promise<NamedOption[]>;
14
14
  /** A company's branches as `{ uuid, name }` options for the posting editor. */
15
15
  listCompanyBranches(companyUuid: string): Promise<NamedOption[]>;
16
+ /**
17
+ * The id of the catalog row named `name`, creating it when none exists (G-009 R5). The catalog
18
+ * is global, so the lookup is by name; the create runs through the engine pipe like any write.
19
+ */
20
+ ensureDepartment(name: string): Promise<string>;
21
+ /** The id of the branch named `name`, creating it when none exists. */
22
+ ensureBranch(name: string): Promise<string>;
23
+ /** Link a department to a company (the junction row); idempotent. */
24
+ attachDepartment(companyUuid: string, departmentUuid: string): Promise<void>;
25
+ /** Link a branch to a company (the junction row); idempotent. */
26
+ attachBranch(companyUuid: string, branchUuid: string): Promise<void>;
27
+ private ensureCatalogRow;
28
+ private attachJunction;
16
29
  /** Shared reader for a company-scoped catalog junction; lists then name-resolves. */
17
30
  private listCompanyCatalog;
18
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,qBACa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,kFAAkF;IAClF,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IASnE,+EAA+E;IAC/E,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAIhE,qFAAqF;YACvE,kBAAkB;CAoBjC"}
1
+ {"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAiB,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAQ7D,qBACa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,kFAAkF;IAClF,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IASnE,+EAA+E;IAC/E,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAIhE;;;OAGG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI/C,uEAAuE;IACvE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI3C,qEAAqE;IACrE,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5E,iEAAiE;IACjE,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAItD,gBAAgB;YAOhB,cAAc;IAa5B,qFAAqF;YACvE,kBAAkB;CAoBjC"}
@@ -18,6 +18,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.OrgCatalogService = void 0;
19
19
  const common_1 = require("@nestjs/common");
20
20
  const engine_1 = require("@astram/engine");
21
+ /** Department/branch rows have a single uuid key; a composite id here is a wiring defect. */
22
+ function catalogId(id) {
23
+ if (typeof id !== 'string')
24
+ throw new TypeError('catalog row id is not a single uuid');
25
+ return id;
26
+ }
21
27
  let OrgCatalogService = class OrgCatalogService {
22
28
  records;
23
29
  constructor(records) {
@@ -31,6 +37,40 @@ let OrgCatalogService = class OrgCatalogService {
31
37
  listCompanyBranches(companyUuid) {
32
38
  return this.listCompanyCatalog('company_branch', 'branchUuid', 'branch', companyUuid);
33
39
  }
40
+ /**
41
+ * The id of the catalog row named `name`, creating it when none exists (G-009 R5). The catalog
42
+ * is global, so the lookup is by name; the create runs through the engine pipe like any write.
43
+ */
44
+ ensureDepartment(name) {
45
+ return this.ensureCatalogRow('department', name);
46
+ }
47
+ /** The id of the branch named `name`, creating it when none exists. */
48
+ ensureBranch(name) {
49
+ return this.ensureCatalogRow('branch', name);
50
+ }
51
+ /** Link a department to a company (the junction row); idempotent. */
52
+ attachDepartment(companyUuid, departmentUuid) {
53
+ return this.attachJunction('company_department', 'departmentUuid', companyUuid, departmentUuid);
54
+ }
55
+ /** Link a branch to a company (the junction row); idempotent. */
56
+ attachBranch(companyUuid, branchUuid) {
57
+ return this.attachJunction('company_branch', 'branchUuid', companyUuid, branchUuid);
58
+ }
59
+ async ensureCatalogRow(typeKey, name) {
60
+ const page = await this.records.list(typeKey, { where: { name } });
61
+ const existing = page.rows[0];
62
+ if (existing !== undefined)
63
+ return catalogId(existing.id);
64
+ return catalogId(await this.records.create(typeKey, { fields: { name } }));
65
+ }
66
+ async attachJunction(junctionTypeKey, refFieldKey, companyUuid, refUuid) {
67
+ const page = await this.records.list(junctionTypeKey, { where: { companyUuid } });
68
+ if (page.rows.some((r) => r.fields[refFieldKey] === refUuid))
69
+ return;
70
+ await this.records.create(junctionTypeKey, {
71
+ fields: { companyUuid, [refFieldKey]: refUuid },
72
+ });
73
+ }
34
74
  /** Shared reader for a company-scoped catalog junction; lists then name-resolves. */
35
75
  async listCompanyCatalog(junctionTypeKey, refFieldKey, catalogTypeKey, companyUuid) {
36
76
  const page = await this.records.list(junctionTypeKey, { where: { companyUuid } });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The ONLY writer of `roles.r_owner_company_uuid` after create (G-009 T14, SPEC R13 v5). The column
3
+ * is immutable at the engine, so an owner change is this audited compare-and-set on the caller's
4
+ * connection (inside its transaction): company -> NULL (company delete) or NULL -> company (owner
5
+ * claim). Any other shape (company -> company, NULL -> NULL, same -> same) is refused with no write.
6
+ * Exactly one row must change; a stale `from` or a failing audit throws, rolling the caller back.
7
+ */
8
+ import { type IdentitySchemaNames } from '../../schema.js';
9
+ export type RoleOwnerQuery = (sql: string, params?: readonly unknown[]) => Promise<unknown>;
10
+ /** Writes the audit row for the transition on the SAME query (connection) as the update. */
11
+ export type RoleOwnerAudit = (action: 'role.owner_cleared' | 'role.owner_claimed', attributes: {
12
+ roleUuid: string;
13
+ from: string | null;
14
+ to: string | null;
15
+ }, query: RoleOwnerQuery) => Promise<void>;
16
+ export declare function transitionRoleOwner(query: RoleOwnerQuery, roleUuid: string, from: string | null, to: string | null, audit: RoleOwnerAudit, schemas?: IdentitySchemaNames): Promise<void>;
17
+ //# sourceMappingURL=role-owner-transition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-owner-transition.d.ts","sourceRoot":"","sources":["../../../src/features/role/role-owner-transition.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEpF,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE5F,4FAA4F;AAC5F,MAAM,MAAM,cAAc,GAAG,CAC3B,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,EACnD,UAAU,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACxE,KAAK,EAAE,cAAc,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,EAAE,EAAE,MAAM,GAAG,IAAI,EACjB,KAAK,EAAE,cAAc,EACrB,OAAO,GAAE,mBAA+C,GACvD,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transitionRoleOwner = transitionRoleOwner;
4
+ /**
5
+ * The ONLY writer of `roles.r_owner_company_uuid` after create (G-009 T14, SPEC R13 v5). The column
6
+ * is immutable at the engine, so an owner change is this audited compare-and-set on the caller's
7
+ * connection (inside its transaction): company -> NULL (company delete) or NULL -> company (owner
8
+ * claim). Any other shape (company -> company, NULL -> NULL, same -> same) is refused with no write.
9
+ * Exactly one row must change; a stale `from` or a failing audit throws, rolling the caller back.
10
+ */
11
+ const schema_js_1 = require("../../schema.js");
12
+ async function transitionRoleOwner(query, roleUuid, from, to, audit, schemas = (0, schema_js_1.frameworkSchemasFromEnv)()) {
13
+ const clearing = from !== null && to === null;
14
+ const claiming = from === null && to !== null;
15
+ if (!clearing && !claiming) {
16
+ throw new Error('A role owner may only change from a company to none, or from none to a company.');
17
+ }
18
+ const res = (await query(`UPDATE \`${schemas.app}\`.\`roles\` SET r_owner_company_uuid = ? WHERE r_uuid = ? AND r_owner_company_uuid <=> ?`, [to, roleUuid, from]));
19
+ if (Number(res?.affectedRows ?? 0) !== 1) {
20
+ throw new Error('The role owner changed concurrently; the transition was not applied.');
21
+ }
22
+ await audit(clearing ? 'role.owner_cleared' : 'role.owner_claimed', { roleUuid, from, to }, query);
23
+ }
@@ -16,7 +16,7 @@ export declare class RoleController {
16
16
  limit: number;
17
17
  }>;
18
18
  /** Create a role — gated on `identity:role:create`. */
19
- createRole(body: {
19
+ createRole(req: AuthedRequest, body: {
20
20
  name?: string;
21
21
  }): Promise<{
22
22
  id: import("packages/engine/dist/types.js").RecordId;
@@ -1 +1 @@
1
- {"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAS,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKhD;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMjC,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
1
+ {"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAM3E;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMjC,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
@@ -38,8 +38,9 @@ let RoleController = class RoleController {
38
38
  return { total: result.total, rows: result.rows, page, limit };
39
39
  }
40
40
  /** Create a role — gated on `identity:role:create`. */
41
- async createRole(body) {
42
- const id = await this.roles.createRole(body?.name ?? 'Untitled');
41
+ async createRole(req, body) {
42
+ // The owner is derived from the principal in the service; any owner in the body is ignored.
43
+ const id = await this.roles.createRole(body?.name ?? 'Untitled', req.principal);
43
44
  return { id };
44
45
  }
45
46
  /** Read one role — gated on `identity:role:read` (CONTROL-plane; root, the identity
@@ -70,9 +71,10 @@ __decorate([
70
71
  __decorate([
71
72
  (0, common_1.Post)('roles'),
72
73
  (0, guard_1.Classified)({ typeKey: 'role', action: 'create' }),
73
- __param(0, (0, common_1.Body)()),
74
+ __param(0, (0, common_1.Req)()),
75
+ __param(1, (0, common_1.Body)()),
74
76
  __metadata("design:type", Function),
75
- __metadata("design:paramtypes", [Object]),
77
+ __metadata("design:paramtypes", [Object, Object]),
76
78
  __metadata("design:returntype", Promise)
77
79
  ], RoleController.prototype, "createRole", null);
78
80
  __decorate([
@@ -20,8 +20,14 @@ export declare class RoleService {
20
20
  private readonly versionBump?;
21
21
  private readonly grants?;
22
22
  constructor(records: RecordsService, ceiling: GrantCeiling, versionBump?: (PostingVersionBump | null) | undefined, grants?: (GrantAdminService | null) | undefined);
23
- /** Create a role. */
24
- createRole(name: string): Promise<RecordId>;
23
+ /**
24
+ * Create a role. The owner is derived here from the actor, never from a client value: a non-root
25
+ * actor's role is owned by its active company (else its tenant); root's role is shared (no owner).
26
+ * A root-flagged actor bound to a declared operation (the org bootstrap's `roles` step, Shield #9)
27
+ * is anchored at the tenant it creates roles for, so its role is owned by that tenant.
28
+ * No actor (internal callers) ⇒ no owner field.
29
+ */
30
+ createRole(name: string, actor?: Principal): Promise<RecordId>;
25
31
  /** List roles (tenant-scoped by the pipe), with paging/search/sort. */
26
32
  listRoles(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
27
33
  /** Read a single role. */
@@ -1 +1 @@
1
- {"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAK/C;;;;;;;;;GASG;AACH,qBACa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAXP,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAKrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,MAAM,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA;IAGpD,qBAAqB;IACrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAI3C,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,8FAA8F;IACxF,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1F;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBhE"}
1
+ {"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAe,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAK/C;;;;;;;;;GASG;AACH,qBACa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAXP,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAKrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,MAAM,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA;IAGpD;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQ9D,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,8FAA8F;IACxF,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1F;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBhE"}
@@ -43,9 +43,19 @@ let RoleService = class RoleService {
43
43
  this.versionBump = versionBump;
44
44
  this.grants = grants;
45
45
  }
46
- /** Create a role. */
47
- createRole(name) {
48
- return this.records.create('role', { fields: { name } });
46
+ /**
47
+ * Create a role. The owner is derived here from the actor, never from a client value: a non-root
48
+ * actor's role is owned by its active company (else its tenant); root's role is shared (no owner).
49
+ * A root-flagged actor bound to a declared operation (the org bootstrap's `roles` step, Shield #9)
50
+ * is anchored at the tenant it creates roles for, so its role is owned by that tenant.
51
+ * No actor (internal callers) ⇒ no owner field.
52
+ */
53
+ createRole(name, actor) {
54
+ const fields = { name };
55
+ if (actor && (actor.isRoot !== true || (0, engine_1.operationOf)(actor) !== undefined)) {
56
+ fields.ownerCompanyUuid = actor.activeScopeNodeId ?? actor.tenantId;
57
+ }
58
+ return this.records.create('role', { fields });
49
59
  }
50
60
  /** List roles (tenant-scoped by the pipe), with paging/search/sort. */
51
61
  listRoles(params) {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DI token for the credential provisioner `UserService.createUserWithCredential` uses (G-009 T7,
3
+ * OD-6): guard's `MariaDbCredentialProvisioner` (`@astram/guard/internal`), built by `IdentityFeatureModule.forRoot()` over
4
+ * the ONE shared authz pool. It writes the locked credential on the engine transaction's own
5
+ * connection, so an API-created user and its credential commit or fail together.
6
+ */
7
+ export declare const IDENTITY_CREDENTIAL_PROVISIONER: unique symbol;
8
+ //# sourceMappingURL=credential-provisioner.token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-provisioner.token.d.ts","sourceRoot":"","sources":["../../../src/features/user/credential-provisioner.token.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,eAA4C,CAAC"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IDENTITY_CREDENTIAL_PROVISIONER = void 0;
4
+ /**
5
+ * DI token for the credential provisioner `UserService.createUserWithCredential` uses (G-009 T7,
6
+ * OD-6): guard's `MariaDbCredentialProvisioner` (`@astram/guard/internal`), built by `IdentityFeatureModule.forRoot()` over
7
+ * the ONE shared authz pool. It writes the locked credential on the engine transaction's own
8
+ * connection, so an API-created user and its credential commit or fail together.
9
+ */
10
+ exports.IDENTITY_CREDENTIAL_PROVISIONER = Symbol('IDENTITY_CREDENTIAL_PROVISIONER');
@@ -0,0 +1,8 @@
1
+ /**
2
+ * G-010 T9 (R9, R11): the invite email. The link is HTML-escaped in the HTML part (guard's
3
+ * escaper is internal to guard, so a local one with the same rule plus `'`); the text part carries
4
+ * the plain link. Only the link and its lifetime appear: no other user data.
5
+ */
6
+ import type { EmailMessage } from '@astram/core';
7
+ export declare function inviteMessage(to: string, link: string, ttlHours: number): EmailMessage;
8
+ //# sourceMappingURL=invite-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite-message.d.ts","sourceRoot":"","sources":["../../../src/features/user/invite-message.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAWjD,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAWtF"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inviteMessage = inviteMessage;
4
+ function escapeHtml(value) {
5
+ return value
6
+ .replace(/&/g, '&amp;')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;')
9
+ .replace(/"/g, '&quot;')
10
+ .replace(/'/g, '&#39;');
11
+ }
12
+ function inviteMessage(to, link, ttlHours) {
13
+ const lifetime = `${ttlHours} ${ttlHours === 1 ? 'hour' : 'hours'}`;
14
+ const intro = 'You have been invited. Set your password to sign in:';
15
+ const expiry = `This link works once and expires in ${lifetime}.`;
16
+ const safe = escapeHtml(link);
17
+ return {
18
+ to,
19
+ subject: 'Set your password',
20
+ text: `${intro} ${link}\n\n${expiry}`,
21
+ html: `<p>${escapeHtml(intro)} <a href="${safe}">${safe}</a></p><p>${escapeHtml(expiry)}</p>`,
22
+ };
23
+ }
@@ -0,0 +1,15 @@
1
+ export interface InviteOptions {
2
+ /** Return the link in the invite response (once, never readable again). Default false. */
3
+ returnLinkToAdmin?: boolean;
4
+ }
5
+ export interface InviteConfig {
6
+ readonly returnLinkToAdmin: boolean;
7
+ readonly ttlHours: number;
8
+ readonly perAdminPerHour: number;
9
+ /** The reset page base, or `null` when it cannot be used (the invite answers 501). */
10
+ readonly linkBase: string | null;
11
+ }
12
+ export declare const DEFAULT_INVITE_LINK_TTL_HOURS = 72;
13
+ export declare const DEFAULT_INVITE_RATE_PER_ADMIN = 30;
14
+ export declare function loadInviteConfig(options?: InviteOptions, env?: NodeJS.ProcessEnv): InviteConfig;
15
+ //# sourceMappingURL=invite.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.config.d.ts","sourceRoot":"","sources":["../../../src/features/user/invite.config.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAChD,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAWhD,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,aAAkB,EAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,YAAY,CAoCd"}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_INVITE_RATE_PER_ADMIN = exports.DEFAULT_INVITE_LINK_TTL_HOURS = void 0;
4
+ exports.loadInviteConfig = loadInviteConfig;
5
+ /**
6
+ * G-010 T9 (R11, P-4, P-5): invite configuration, read ONCE by `IdentityFeatureModule.forRoot`.
7
+ *
8
+ * - `INVITE_LINK_TTL_HOURS`: link lifetime, whole hours 1 to 168 (default 72; the G-009 rule).
9
+ * - `INVITE_RATE_PER_ADMIN`: invites one administrator may send per 60 minutes, 1 to 1000
10
+ * (default 30).
11
+ * - The link base follows forgot-password's `RESET_LINK_BASE_URL` rule (G-009
12
+ * `resolveSetPasswordBase`). When it cannot be resolved the invite answers 501, unless the app
13
+ * opted into `returnLinkToAdmin`, which then refuses boot (it promised a usable link).
14
+ *
15
+ * A bad value refuses boot with an error naming the variable, never echoing the value.
16
+ */
17
+ const set_password_links_js_1 = require("../org-bootstrap/set-password-links.js");
18
+ exports.DEFAULT_INVITE_LINK_TTL_HOURS = 72;
19
+ exports.DEFAULT_INVITE_RATE_PER_ADMIN = 30;
20
+ const MAX_INVITE_RATE_PER_ADMIN = 1000;
21
+ function wholeNumber(raw, name, fallback) {
22
+ if (raw === undefined)
23
+ return fallback;
24
+ if (!/^\d+$/.test(raw)) {
25
+ throw new Error(`${name} must be a plain positive whole number.`);
26
+ }
27
+ return Number(raw);
28
+ }
29
+ function loadInviteConfig(options = {}, env = process.env) {
30
+ const ttlRaw = wholeNumber(env.INVITE_LINK_TTL_HOURS, 'INVITE_LINK_TTL_HOURS', exports.DEFAULT_INVITE_LINK_TTL_HOURS);
31
+ let ttlHours;
32
+ try {
33
+ ttlHours = (0, set_password_links_js_1.validateLinkTtlHours)(ttlRaw);
34
+ }
35
+ catch {
36
+ throw new Error('INVITE_LINK_TTL_HOURS must be 1 to 168 whole hours.');
37
+ }
38
+ const perAdminPerHour = wholeNumber(env.INVITE_RATE_PER_ADMIN, 'INVITE_RATE_PER_ADMIN', exports.DEFAULT_INVITE_RATE_PER_ADMIN);
39
+ if (perAdminPerHour < 1 || perAdminPerHour > MAX_INVITE_RATE_PER_ADMIN) {
40
+ throw new Error(`INVITE_RATE_PER_ADMIN must be 1 to ${MAX_INVITE_RATE_PER_ADMIN}.`);
41
+ }
42
+ const returnLinkToAdmin = options.returnLinkToAdmin === true;
43
+ let linkBase;
44
+ try {
45
+ linkBase = (0, set_password_links_js_1.resolveSetPasswordBase)(env);
46
+ }
47
+ catch (err) {
48
+ if (returnLinkToAdmin) {
49
+ throw new Error('IdentityFeatureModule.forRoot({ invite: { returnLinkToAdmin: true } }): ' +
50
+ (err instanceof Error ? err.message : 'RESET_LINK_BASE_URL is not usable.'));
51
+ }
52
+ linkBase = null;
53
+ }
54
+ return { returnLinkToAdmin, ttlHours, perAdminPerHour, linkBase };
55
+ }
@@ -0,0 +1,62 @@
1
+ import { RecordsService } from '@astram/engine';
2
+ import type { AuditEvent, Principal } from '@astram/core';
3
+ import { type CredentialProvisioner, type MessageDispatcher, type ResetTokenStore } from '@astram/guard';
4
+ import { GrantCeiling } from '../authz/grant-ceiling.js';
5
+ import type { InviteConfig } from './invite.config.js';
6
+ /**
7
+ * The slice of guard's `AttemptLimiter` the invite uses: `consume` is one atomic Lua step
8
+ * (`ATTEMPT_CONSUME_SCRIPT`: INCR, window TTL armed on the first hit and re-armed when the key has
9
+ * none) that answers whether this reservation is within the budget. It REJECTS when Redis cannot
10
+ * answer (`AttemptLimiterUnavailableError`); the invite then proceeds (fail open, P-5).
11
+ */
12
+ export interface InviteLimiter {
13
+ consume(key: string): Promise<{
14
+ readonly allowed: boolean;
15
+ readonly count: number;
16
+ }>;
17
+ }
18
+ export interface InviteLimiters {
19
+ readonly user: InviteLimiter;
20
+ readonly admin: InviteLimiter;
21
+ }
22
+ export interface InviteAuditWriter {
23
+ record(event: AuditEvent): Promise<void>;
24
+ }
25
+ /** The route's response. `link` only when the app opted into `returnLinkToAdmin`. */
26
+ export interface InviteResult {
27
+ emailed: boolean;
28
+ link?: string;
29
+ }
30
+ /** Invites per invited user per window (matches forgot-password's per-account limit). */
31
+ export declare const INVITES_PER_USER = 3;
32
+ export declare const INVITE_WINDOW_MS = 3600000;
33
+ export declare class InviteService {
34
+ private readonly records;
35
+ private readonly ceiling;
36
+ private readonly config;
37
+ private readonly limiters?;
38
+ private readonly audit?;
39
+ private readonly resetTokens?;
40
+ private readonly provisioner?;
41
+ private readonly dispatcher?;
42
+ private readonly logger;
43
+ constructor(records: RecordsService, ceiling: GrantCeiling, config: InviteConfig, limiters?: (InviteLimiters | null) | undefined, audit?: (InviteAuditWriter | null) | undefined, resetTokens?: (ResetTokenStore | null) | undefined, provisioner?: (CredentialProvisioner | null) | undefined, dispatcher?: (MessageDispatcher | null) | undefined);
44
+ invite(actor: Principal, uuid: string): Promise<InviteResult>;
45
+ /** Only a locked credential with an email can be invited; otherwise 409 with the reason. */
46
+ private assertInvitable;
47
+ /** Reserve one slot; `false` = over the budget. A limiter that cannot answer fails open (P-5). */
48
+ private reserve;
49
+ /**
50
+ * Burn this invite's own token (the store's single-use consume of exactly that hash). A token a
51
+ * newer invite already invalidated is simply not found. A store error is logged, not thrown: the
52
+ * caller still answers 503 and nobody holds the link.
53
+ */
54
+ private burn;
55
+ private throttled;
56
+ /**
57
+ * Writes one audit row; never throws. Returns whether the row was written (`false` when no
58
+ * writer is wired or the sink failed), so the link-return path can fail closed on it.
59
+ */
60
+ private record;
61
+ }
62
+ //# sourceMappingURL=invite.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/invite.service.ts"],"names":[],"mappings":"AAuCA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAOzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQvD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtF;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,qFAAqF;AACrF,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,yFAAyF;AACzF,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,eAAO,MAAM,gBAAgB,UAAY,CAAC;AAW1C,qBACa,aAAa;IAItB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACQ,OAAO,CAAC,QAAQ,CAAC,MAAM;IAGvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACiB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IAGlE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAG7B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAG7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAlB9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;gBAGtC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EACW,MAAM,EAAE,YAAY,EAGpD,QAAQ,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA,EACW,KAAK,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA,EAG3E,WAAW,CAAC,GAAE,eAAe,GAAG,IAAI,aAAA,EAGpC,WAAW,CAAC,GAAE,qBAAqB,GAAG,IAAI,aAAA,EAG1C,UAAU,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA;IAGlD,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAqGnE,4FAA4F;IAC5F,OAAO,CAAC,eAAe;IAKvB,kGAAkG;YACpF,OAAO;IASrB;;;;OAIG;YACW,IAAI;YAQJ,SAAS;IAKvB;;;OAGG;YACW,MAAM;CAuBrB"}