@managemint-solutions/sdk 0.53.0 → 0.55.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 (60) hide show
  1. package/README.md +75 -3
  2. package/dist/additional-details/index.d.ts +2 -2
  3. package/dist/additional-details/index.js +6 -10
  4. package/dist/audit/index.d.ts +11 -5
  5. package/dist/audit/index.js +20 -12
  6. package/dist/auth-client/index.d.ts +2 -0
  7. package/dist/auth-client/index.js +12 -0
  8. package/dist/auth.d.ts +21 -0
  9. package/dist/auth.js +47 -3
  10. package/dist/clients/dto.d.ts +5 -1
  11. package/dist/clients/dto.js +12 -1
  12. package/dist/clients/index.d.ts +5 -3
  13. package/dist/clients/index.js +12 -10
  14. package/dist/emulation-handshakes/index.d.ts +16 -0
  15. package/dist/emulation-handshakes/index.js +39 -0
  16. package/dist/errors.js +2 -2
  17. package/dist/index.d.ts +13 -9
  18. package/dist/index.js +22 -4
  19. package/dist/leads/dto.d.ts +5 -1
  20. package/dist/leads/dto.js +12 -1
  21. package/dist/leads/index.d.ts +5 -3
  22. package/dist/leads/index.js +24 -21
  23. package/dist/nest/index.d.ts +26 -1
  24. package/dist/nest/index.js +58 -1
  25. package/dist/notifications/index.d.ts +1 -3
  26. package/dist/organizations/dto.d.ts +9 -1
  27. package/dist/organizations/dto.js +40 -1
  28. package/dist/organizations/index.d.ts +11 -3
  29. package/dist/organizations/index.js +30 -2
  30. package/dist/permissions/index.d.ts +6 -4
  31. package/dist/permissions/index.js +17 -12
  32. package/dist/projects/dto.d.ts +5 -1
  33. package/dist/projects/dto.js +12 -1
  34. package/dist/projects/index.d.ts +5 -3
  35. package/dist/projects/index.js +12 -10
  36. package/dist/service-client/index.d.ts +32 -1
  37. package/dist/service-client/index.js +54 -1
  38. package/dist/staff/dto.d.ts +14 -0
  39. package/dist/staff/dto.js +63 -0
  40. package/dist/staff/index.d.ts +50 -0
  41. package/dist/staff/index.js +139 -0
  42. package/dist/staff-client.d.ts +37 -0
  43. package/dist/staff-client.js +52 -0
  44. package/dist/tasks/dto.d.ts +5 -1
  45. package/dist/tasks/dto.js +12 -1
  46. package/dist/tasks/index.d.ts +5 -3
  47. package/dist/tasks/index.js +18 -19
  48. package/dist/tenancy.d.ts +13 -0
  49. package/dist/tenancy.js +25 -0
  50. package/dist/user-api-credentials/dto.d.ts +12 -0
  51. package/dist/user-api-credentials/dto.js +56 -0
  52. package/dist/user-api-credentials/index.d.ts +47 -0
  53. package/dist/user-api-credentials/index.js +101 -0
  54. package/dist/users/dto.d.ts +5 -1
  55. package/dist/users/dto.js +13 -2
  56. package/dist/users/index.d.ts +5 -3
  57. package/dist/users/index.js +13 -29
  58. package/dist/validators.d.ts +2 -0
  59. package/dist/validators.js +6 -1
  60. package/package.json +2 -2
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.UpdateOrganizationDto = exports.CreateOrganizationDto = void 0;
12
+ exports.GetOrganizationsDto = exports.OrganizationIdDto = exports.UpdateOrganizationDto = exports.CreateOrganizationDto = void 0;
13
13
  const enum_1 = require("@managemint-solutions/entities/organization/enum");
14
14
  const class_validator_1 = require("class-validator");
15
15
  const validators_1 = require("../validators");
@@ -450,3 +450,42 @@ __decorate([
450
450
  (0, class_validator_1.Max)(12, { message: 'Leave year start month must be between 1 and 12' }),
451
451
  __metadata("design:type", Number)
452
452
  ], UpdateOrganizationDto.prototype, "leave_calendar_start_month", void 0);
453
+ // No tenant route takes one - a tenant only ever reaches their own organization - so this is the
454
+ // admin api's `/organizations/:organizationId`.
455
+ class OrganizationIdDto {
456
+ organizationId;
457
+ }
458
+ exports.OrganizationIdDto = OrganizationIdDto;
459
+ __decorate([
460
+ (0, class_validator_1.IsUUID)('7', { message: 'Invalid organization ID format' }),
461
+ __metadata("design:type", String)
462
+ ], OrganizationIdDto.prototype, "organizationId", void 0);
463
+ class GetOrganizationsDto {
464
+ search;
465
+ page = 1;
466
+ page_size = 10;
467
+ }
468
+ exports.GetOrganizationsDto = GetOrganizationsDto;
469
+ __decorate([
470
+ (0, class_validator_1.IsOptional)(),
471
+ (0, validators_1.IsNullable)(),
472
+ (0, class_validator_1.IsString)({ message: 'Search query must be a string' }),
473
+ (0, class_validator_1.MinLength)(1, { message: 'Search query cannot be empty' }),
474
+ (0, class_validator_1.MaxLength)(255, { message: 'Search query cannot exceed 255 characters' }),
475
+ __metadata("design:type", Object)
476
+ ], GetOrganizationsDto.prototype, "search", void 0);
477
+ __decorate([
478
+ (0, class_validator_1.IsOptional)(),
479
+ (0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 1, { toClassOnly: true }),
480
+ (0, class_validator_1.IsNumber)({}, { message: 'Page must be a number' }),
481
+ (0, class_validator_1.Min)(1, { message: 'Page must be at least 1' }),
482
+ __metadata("design:type", Number)
483
+ ], GetOrganizationsDto.prototype, "page", void 0);
484
+ __decorate([
485
+ (0, class_validator_1.IsOptional)(),
486
+ (0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 10, { toClassOnly: true }),
487
+ (0, class_validator_1.IsNumber)({}, { message: 'Page size must be a number' }),
488
+ (0, class_validator_1.Min)(1, { message: 'Page size must be at least 1' }),
489
+ (0, class_validator_1.Max)(100, { message: 'Page size cannot exceed 100' }),
490
+ __metadata("design:type", Number)
491
+ ], GetOrganizationsDto.prototype, "page_size", void 0);
@@ -1,6 +1,9 @@
1
1
  import type { SeatReservation } from '@managemint-solutions/entities/billing';
2
- import { type AuditActor, type AuditResource, type AuditRow, EntityAuditTrail } from '../audit';
2
+ import type { MmsList } from '@managemint-solutions/entities/common';
3
+ import type { OrganizationSummary } from '@managemint-solutions/entities/organization';
4
+ import { type AuditResource, type AuditRow, EntityAuditTrail, type ResourceActor } from '../audit';
3
5
  import type { TypedSupabaseClient } from '../client';
6
+ import type { GetOrganizationsDto } from './dto';
4
7
  export * from './dto';
5
8
  /** The organizations row as `ORGANIZATION_SELECT` produces it, before the api shapes it. */
6
9
  export type OrganizationRow = Record<string, unknown> & {
@@ -20,10 +23,15 @@ export declare class OrganizationsResource {
20
23
  private readonly table;
21
24
  /** This record's history - its own rows and the rows that hang off it. */
22
25
  readonly audit: EntityAuditTrail;
23
- constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
26
+ constructor(supabase: TypedSupabaseClient, auth: ResourceActor, audit: AuditResource);
24
27
  getById(organizationId: string): Promise<OrganizationRow>;
25
- /** The caller's own organization, from the auth context. */
28
+ /** The caller's own organization, from the auth context. Staff have none: a 403. */
26
29
  get(): Promise<OrganizationRow>;
30
+ /**
31
+ * Every organization the caller can read, newest first - all of them for staff, the caller's
32
+ * own for a tenant, as RLS decides. `search` matches the name, trading name and email.
33
+ */
34
+ list(query: GetOrganizationsDto): Promise<MmsList<OrganizationSummary>>;
27
35
  /**
28
36
  * The whole row, unprojected. Billing reads a different handful of columns for each of its
29
37
  * flows - provisioning, a module purchase, a seat charge - and they are all single-row
@@ -18,6 +18,7 @@ exports.OrganizationSignupResource = exports.OrganizationsResource = void 0;
18
18
  const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
19
19
  const audit_1 = require("../audit");
20
20
  const errors_1 = require("../errors");
21
+ const tenancy_1 = require("../tenancy");
21
22
  const transforms_1 = require("../transforms");
22
23
  __exportStar(require("./dto"), exports);
23
24
  // The subscription machinery on the row - what the billing flows and the Paystack webhooks
@@ -94,6 +95,8 @@ const ORGANIZATION_SELECT = `
94
95
  card_expiring_soon,
95
96
  pricebook_id
96
97
  `;
98
+ // The public contract is the shared OrganizationSummary; `modules` stays the stored module keys.
99
+ const ORGANIZATION_LIST_SELECT = 'mms_id, created_at, name, trading_name, email, billing_status, active_seat_count, paid_seat_count, modules';
97
100
  /**
98
101
  * The caller's own organization. Every method takes the id explicitly rather than reading it
99
102
  * off the auth context, because the signup and webhook paths operate on an organization that
@@ -121,9 +124,34 @@ class OrganizationsResource {
121
124
  throw (0, errors_1.mapPostgrestError)(error);
122
125
  return data;
123
126
  }
124
- /** The caller's own organization, from the auth context. */
127
+ /** The caller's own organization, from the auth context. Staff have none: a 403. */
125
128
  async get() {
126
- return this.getById(this.auth.organization_id);
129
+ return this.getById((0, tenancy_1.requireOrganization)(this.auth));
130
+ }
131
+ /**
132
+ * Every organization the caller can read, newest first - all of them for staff, the caller's
133
+ * own for a tenant, as RLS decides. `search` matches the name, trading name and email.
134
+ */
135
+ async list(query) {
136
+ const page = query.page;
137
+ const pageSize = query.page_size;
138
+ const offset = (page - 1) * pageSize;
139
+ const trimmedSearch = query.search?.trim();
140
+ // `order`/`range` return `this`, so the search can be reassigned onto the builder without a
141
+ // cast. Exact, not estimated, like invoices: there are tens of organizations, not tens of
142
+ // thousands, and the pager must not offer a page that is not there.
143
+ let organizationsQuery = this.supabase
144
+ .from('organizations')
145
+ .select(ORGANIZATION_LIST_SELECT, { count: 'exact' })
146
+ .order('created_at', { ascending: false })
147
+ .range(offset, offset + pageSize - 1);
148
+ if (trimmedSearch) {
149
+ organizationsQuery = organizationsQuery.or((0, transforms_1.orIlike)(['name', 'trading_name', 'email'], trimmedSearch));
150
+ }
151
+ const { data, error, count } = await organizationsQuery;
152
+ if (error)
153
+ throw (0, errors_1.mapPostgrestError)(error);
154
+ return (0, transforms_1.toMmsList)(data, page, pageSize, count);
127
155
  }
128
156
  /**
129
157
  * The whole row, unprojected. Billing reads a different handful of columns for each of its
@@ -7,6 +7,12 @@ export type PermissionFlags = {
7
7
  is_owner: boolean;
8
8
  is_admin: boolean;
9
9
  } & Record<string, boolean>;
10
+ /**
11
+ * A guard's flag is interpolated into a select string, so it is checked against the shape a
12
+ * column name can take first - a flag never comes from a request, only from a route's own
13
+ * decorator, so anything else is a bug rather than a bad request.
14
+ */
15
+ export declare const checkPermissionFlag: (flag: string) => void;
10
16
  /**
11
17
  * The `permissions` table. Rows are created by the `on_auth_user_created` trigger and deleted
12
18
  * with the user - INSERT and DELETE are `false` for `authenticated` - so this resource only
@@ -26,10 +32,6 @@ export declare class PermissionsResource {
26
32
  * The one lookup `PermissionsGuard` makes per gated request: the flag the route asked for,
27
33
  * plus the two that bypass it. Returns null when the caller has no permissions row, which the
28
34
  * guard treats as a denial.
29
- *
30
- * `flag` is interpolated into the select string, so it is checked against the shape a column
31
- * name can take first - a flag never comes from a request, only from a route's own decorator,
32
- * so anything else is a bug rather than a bad request.
33
35
  */
34
36
  flagsFor(userId: string, flag: string): Promise<PermissionFlags | null>;
35
37
  /** Whether a user owns the organization - the guard behind "you cannot deactivate the owner". */
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.PermissionsResource = void 0;
17
+ exports.PermissionsResource = exports.checkPermissionFlag = void 0;
18
18
  const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
19
19
  const audit_1 = require("../audit");
20
20
  const errors_1 = require("../errors");
@@ -25,6 +25,21 @@ const PERMISSIONS_TABLE = {
25
25
  // A permissions change shows in the history of the user it belongs to.
26
26
  related: { entityType: enum_1.AuditTrailEntityType.USER, column: 'user_id' },
27
27
  };
28
+ /**
29
+ * A guard's flag is interpolated into a select string, so it is checked against the shape a
30
+ * column name can take first - a flag never comes from a request, only from a route's own
31
+ * decorator, so anything else is a bug rather than a bad request.
32
+ */
33
+ const checkPermissionFlag = (flag) => {
34
+ if (!/^[a-z][a-z0-9_]*$/.test(flag)) {
35
+ throw new errors_1.SupabaseClientError({
36
+ status: 500,
37
+ error: 'Internal Error',
38
+ message: 'Invalid permission flag',
39
+ });
40
+ }
41
+ };
42
+ exports.checkPermissionFlag = checkPermissionFlag;
28
43
  /**
29
44
  * The `permissions` table. Rows are created by the `on_auth_user_created` trigger and deleted
30
45
  * with the user - INSERT and DELETE are `false` for `authenticated` - so this resource only
@@ -58,19 +73,9 @@ class PermissionsResource {
58
73
  * The one lookup `PermissionsGuard` makes per gated request: the flag the route asked for,
59
74
  * plus the two that bypass it. Returns null when the caller has no permissions row, which the
60
75
  * guard treats as a denial.
61
- *
62
- * `flag` is interpolated into the select string, so it is checked against the shape a column
63
- * name can take first - a flag never comes from a request, only from a route's own decorator,
64
- * so anything else is a bug rather than a bad request.
65
76
  */
66
77
  async flagsFor(userId, flag) {
67
- if (!/^[a-z][a-z0-9_]*$/.test(flag)) {
68
- throw new errors_1.SupabaseClientError({
69
- status: 500,
70
- error: 'Internal Error',
71
- message: 'Invalid permission flag',
72
- });
73
- }
78
+ (0, exports.checkPermissionFlag)(flag);
74
79
  const { data, error } = await this.supabase
75
80
  .from('permissions')
76
81
  .select(`is_owner, is_admin, ${flag}`)
@@ -1,4 +1,4 @@
1
- import type { CreateProjectDto as CreateProjectDtoType, GetProjectsDto as GetProjectsDtoType, ProjectIdDto as ProjectIdDtoType, UpdateProjectDto as UpdateProjectDtoType } from '@managemint-solutions/entities/projects/dto';
1
+ import type { CreateProjectDto as CreateProjectDtoType, GetProjectsDto as GetProjectsDtoType, StaffGetProjectsDto as StaffGetProjectsDtoType, ProjectIdDto as ProjectIdDtoType, UpdateProjectDto as UpdateProjectDtoType } from '@managemint-solutions/entities/projects/dto';
2
2
  export declare class GetProjectsDto implements Omit<GetProjectsDtoType, 'status_id_in' | 'status_id_not_in'> {
3
3
  readonly search?: string | null;
4
4
  readonly page: number;
@@ -15,6 +15,10 @@ export declare class GetProjectsDto implements Omit<GetProjectsDtoType, 'status_
15
15
  readonly client_id?: string | null;
16
16
  readonly archive: boolean;
17
17
  }
18
+ /** A staff read across organizations, optionally narrowed to one of them. */
19
+ export declare class StaffGetProjectsDto extends GetProjectsDto implements Omit<StaffGetProjectsDtoType, 'status_id_in' | 'status_id_not_in'> {
20
+ readonly organization_id?: string | null;
21
+ }
18
22
  export declare class ProjectIdDto implements ProjectIdDtoType {
19
23
  readonly projectId: string;
20
24
  }
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.UpdateProjectDto = exports.CreateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = void 0;
12
+ exports.UpdateProjectDto = exports.CreateProjectDto = exports.ProjectIdDto = exports.StaffGetProjectsDto = exports.GetProjectsDto = void 0;
13
13
  const class_validator_1 = require("class-validator");
14
14
  const class_transformer_1 = require("class-transformer");
15
15
  const transforms_1 = require("../transforms");
@@ -132,6 +132,17 @@ __decorate([
132
132
  (0, class_validator_1.IsBoolean)({ message: 'Include archived must be a boolean' }),
133
133
  __metadata("design:type", Boolean)
134
134
  ], GetProjectsDto.prototype, "archive", void 0);
135
+ /** A staff read across organizations, optionally narrowed to one of them. */
136
+ class StaffGetProjectsDto extends GetProjectsDto {
137
+ organization_id;
138
+ }
139
+ exports.StaffGetProjectsDto = StaffGetProjectsDto;
140
+ __decorate([
141
+ (0, class_validator_1.IsOptional)(),
142
+ (0, validators_1.IsNullable)(),
143
+ (0, class_validator_1.IsUUID)('7', { message: 'Invalid organization ID format' }),
144
+ __metadata("design:type", Object)
145
+ ], StaffGetProjectsDto.prototype, "organization_id", void 0);
135
146
  class ProjectIdDto {
136
147
  projectId;
137
148
  }
@@ -1,6 +1,6 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { ProjectEntity, ProjectSummary } from '@managemint-solutions/entities/projects';
3
- import type { AuthContext } from '../auth';
3
+ import type { ClientContext } from '../auth';
4
4
  import { EntityAuditTrail, type AuditResource } from '../audit';
5
5
  import type { TypedSupabaseClient } from '../client';
6
6
  import type { CreateProjectDto, GetProjectsDto, UpdateProjectDto } from './dto';
@@ -11,8 +11,10 @@ export declare class ProjectsResource {
11
11
  private readonly table;
12
12
  /** This record's history - its own rows and the rows that hang off it. */
13
13
  readonly audit: EntityAuditTrail;
14
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
15
- list(query: GetProjectsDto): Promise<MmsList<ProjectSummary>>;
14
+ constructor(supabase: TypedSupabaseClient, auth: ClientContext, audit: AuditResource);
15
+ list(query: GetProjectsDto & {
16
+ organization_id?: string | null;
17
+ }): Promise<MmsList<ProjectSummary>>;
16
18
  getById(projectId: string): Promise<ProjectEntity>;
17
19
  create(input: CreateProjectDto): Promise<ProjectEntity>;
18
20
  update(projectId: string, input: UpdateProjectDto): Promise<ProjectEntity>;
@@ -18,6 +18,7 @@ exports.ProjectsResource = void 0;
18
18
  const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
19
19
  const audit_1 = require("../audit");
20
20
  const errors_1 = require("../errors");
21
+ const tenancy_1 = require("../tenancy");
21
22
  const transforms_1 = require("../transforms");
22
23
  __exportStar(require("./dto"), exports);
23
24
  // The public contract is the shared ProjectSummary / ProjectEntity from
@@ -97,14 +98,16 @@ class ProjectsResource {
97
98
  const trimmedSearch = query.search?.trim();
98
99
  // `order`/`range` return `this`, so the builder stays a filter builder and the conditional
99
100
  // filters below can be reassigned onto it without a cast.
100
- let projectsQuery = this.supabase
101
+ let projectsQuery = (0, tenancy_1.scopeToOrganization)(this.supabase
101
102
  .from('projects')
102
- .select(PROJECT_LIST_SELECT, { count: 'estimated' })
103
- .eq('organization_id', this.auth.organization_id)
103
+ .select((0, tenancy_1.withOrganization)(PROJECT_LIST_SELECT, this.auth), { count: 'estimated' }), this.auth)
104
104
  .eq('archived', query.archive)
105
105
  .eq('deleted', false)
106
106
  .range(offset, offset + pageSize - 1)
107
107
  .order('created_at', { ascending: false });
108
+ if (query.organization_id) {
109
+ projectsQuery = projectsQuery.eq('organization_id', query.organization_id);
110
+ }
108
111
  if (trimmedSearch) {
109
112
  projectsQuery = projectsQuery.or((0, transforms_1.orIlike)(['name', 'specification'], trimmedSearch));
110
113
  }
@@ -147,18 +150,17 @@ class ProjectsResource {
147
150
  return (0, transforms_1.toMmsList)(data, page, pageSize, count);
148
151
  }
149
152
  async getById(projectId) {
150
- const { data, error } = await this.supabase
151
- .from('projects')
152
- .select(PROJECT_DETAIL_SELECT)
153
- .eq('organization_id', this.auth.organization_id)
153
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('projects').select((0, tenancy_1.withOrganization)(PROJECT_DETAIL_SELECT, this.auth)), this.auth)
154
154
  .eq('mms_id', projectId)
155
155
  .single();
156
156
  if (error)
157
157
  throw (0, errors_1.mapPostgrestError)(error);
158
+ // The select carries the staff embed, so its row type is not inferred from the string.
159
+ const row = data;
158
160
  // A soft-deleted project keeps its creation audit but gives up its details.
159
- if (data.deleted) {
161
+ if (row.deleted) {
160
162
  return {
161
- ...data,
163
+ ...row,
162
164
  name: '',
163
165
  specification: null,
164
166
  has_budget: false,
@@ -182,7 +184,7 @@ class ProjectsResource {
182
184
  ...input,
183
185
  start_date: (0, transforms_1.toDateInput)(input.start_date),
184
186
  end_date: (0, transforms_1.toDateInput)(input.end_date),
185
- organization_id: this.auth.organization_id,
187
+ organization_id: (0, tenancy_1.requireOrganization)(this.auth),
186
188
  created_by: this.auth.mms_id,
187
189
  created_at: new Date().toISOString(),
188
190
  });
@@ -4,6 +4,7 @@ import { BillingResource } from '../billing';
4
4
  import { InvoicesResource } from '../billing/invoices';
5
5
  import { ClientsResource } from '../clients';
6
6
  import { ConfigsResource } from '../configs';
7
+ import { EmulationHandshakesResource } from '../emulation-handshakes';
7
8
  import { FeatureRequestsResource } from '../feature-requests';
8
9
  import { LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from '../leave';
9
10
  import { NotificationsOutboxResource } from '../notifications';
@@ -11,6 +12,8 @@ import { OrganizationInvitesResource } from '../organization-invites';
11
12
  import { OrganizationPaymentMethodsResource, type PaymentMethodLookupColumn } from '../organization-payment-methods';
12
13
  import { OrganizationsResource, OrganizationSignupResource } from '../organizations';
13
14
  import { PermissionsResource } from '../permissions';
15
+ import { StaffPermissionsResource, StaffResource } from '../staff';
16
+ import { UserApiCredentialsResource } from '../user-api-credentials';
14
17
  import { UsersResource } from '../users';
15
18
  import { WebhookEventsResource } from '../webhook-events';
16
19
  export type SupabaseServiceClientConfig = {
@@ -61,7 +64,8 @@ export declare class SupabaseOrganizationScope {
61
64
  /**
62
65
  * The client for work that has no caller and no RLS context to run under: inbound webhooks
63
66
  * (Paystack, Resend, the Supabase `send_email` hook), the notification queue worker, unauthenticated
64
- * signup, the website's feature-request intake, and the writes RLS reserves for the service role
67
+ * signup, the website's feature-request intake, the admin api's writes (staff, their flags, user
68
+ * api credentials, the emulation mint), and the writes RLS reserves for the service role
65
69
  * outright - the ledger, the outbox, the webhook event logs, `organizations` INSERT, `permissions`
66
70
  * INSERT/DELETE.
67
71
  *
@@ -78,7 +82,34 @@ export declare class SupabaseServiceClient {
78
82
  readonly signup: OrganizationSignupResource;
79
83
  readonly paystackEvents: WebhookEventsResource;
80
84
  readonly resendEvents: WebhookEventsResource;
85
+ readonly staff: StaffResource;
86
+ readonly staffPermissions: StaffPermissionsResource;
87
+ readonly userApiCredentials: UserApiCredentialsResource;
88
+ readonly emulationHandshakes: EmulationHandshakesResource;
81
89
  constructor(config: SupabaseServiceClientConfig);
90
+ /**
91
+ * A one-time `token_hash` that signs `email` in - the admin api verifies it itself, at once,
92
+ * through `SupabaseAuthClient.exchangeMagicLinkToken`. Nothing is sent: the link only exists
93
+ * to be verified.
94
+ */
95
+ mintMagicLink(email: string): Promise<{
96
+ token_hash: string;
97
+ }>;
98
+ /**
99
+ * The auth user behind a staff pre-registration (`staff.preRegister`), confirmed and with the
100
+ * `staff` role - which is what links the two (`handle_auth_user_role_set`).
101
+ */
102
+ createStaffAuthUser(input: {
103
+ email: string;
104
+ password: string;
105
+ }): Promise<{
106
+ id: string;
107
+ }>;
108
+ /**
109
+ * Bans or unbans an auth user, so a deactivated user or staff member cannot sign in or refresh.
110
+ * A ban has no "forever" in GoTrue; a hundred years is the usual stand-in.
111
+ */
112
+ setAuthUserBanned(userId: string, banned: boolean): Promise<void>;
82
113
  /**
83
114
  * Scope to one organization once it is known. `actorMmsId` stays `null` for system work -
84
115
  * `audit_logs.created_by` is nullable precisely so an unattributed write is still recorded.
@@ -8,6 +8,7 @@ const billing_1 = require("../billing");
8
8
  const invoices_1 = require("../billing/invoices");
9
9
  const clients_1 = require("../clients");
10
10
  const configs_1 = require("../configs");
11
+ const emulation_handshakes_1 = require("../emulation-handshakes");
11
12
  const feature_requests_1 = require("../feature-requests");
12
13
  const leave_1 = require("../leave");
13
14
  const notifications_1 = require("../notifications");
@@ -16,6 +17,8 @@ const organization_invites_1 = require("../organization-invites");
16
17
  const organization_payment_methods_1 = require("../organization-payment-methods");
17
18
  const organizations_1 = require("../organizations");
18
19
  const permissions_1 = require("../permissions");
20
+ const staff_1 = require("../staff");
21
+ const user_api_credentials_1 = require("../user-api-credentials");
19
22
  const users_1 = require("../users");
20
23
  const webhook_events_1 = require("../webhook-events");
21
24
  /**
@@ -91,7 +94,8 @@ exports.SupabaseOrganizationScope = SupabaseOrganizationScope;
91
94
  /**
92
95
  * The client for work that has no caller and no RLS context to run under: inbound webhooks
93
96
  * (Paystack, Resend, the Supabase `send_email` hook), the notification queue worker, unauthenticated
94
- * signup, the website's feature-request intake, and the writes RLS reserves for the service role
97
+ * signup, the website's feature-request intake, the admin api's writes (staff, their flags, user
98
+ * api credentials, the emulation mint), and the writes RLS reserves for the service role
95
99
  * outright - the ledger, the outbox, the webhook event logs, `organizations` INSERT, `permissions`
96
100
  * INSERT/DELETE.
97
101
  *
@@ -111,6 +115,10 @@ class SupabaseServiceClient {
111
115
  signup;
112
116
  paystackEvents;
113
117
  resendEvents;
118
+ staff;
119
+ staffPermissions;
120
+ userApiCredentials;
121
+ emulationHandshakes;
114
122
  constructor(config) {
115
123
  this.config = config;
116
124
  this.supabase = (0, auth_1.createRawClient)(config.url, config.serviceRoleKey);
@@ -125,6 +133,51 @@ class SupabaseServiceClient {
125
133
  table: 'notifications',
126
134
  entityType: enum_1.AuditTrailEntityType.NOTIFICATION,
127
135
  }));
136
+ this.staff = new staff_1.StaffResource(this.supabase);
137
+ this.staffPermissions = new staff_1.StaffPermissionsResource(this.supabase);
138
+ this.userApiCredentials = new user_api_credentials_1.UserApiCredentialsResource(this.supabase, (organizationId) => this.forOrganization(organizationId).table({
139
+ table: 'user_api_credentials',
140
+ entityType: enum_1.AuditTrailEntityType.USER_API_CREDENTIAL,
141
+ related: { entityType: enum_1.AuditTrailEntityType.USER, column: 'user_id' },
142
+ }));
143
+ this.emulationHandshakes = new emulation_handshakes_1.EmulationHandshakesResource(this.supabase);
144
+ }
145
+ /**
146
+ * A one-time `token_hash` that signs `email` in - the admin api verifies it itself, at once,
147
+ * through `SupabaseAuthClient.exchangeMagicLinkToken`. Nothing is sent: the link only exists
148
+ * to be verified.
149
+ */
150
+ async mintMagicLink(email) {
151
+ const { data, error } = await this.supabase.auth.admin.generateLink({ type: 'magiclink', email });
152
+ if (error)
153
+ throw (0, errors_1.mapAuthError)(error);
154
+ return { token_hash: data.properties.hashed_token };
155
+ }
156
+ /**
157
+ * The auth user behind a staff pre-registration (`staff.preRegister`), confirmed and with the
158
+ * `staff` role - which is what links the two (`handle_auth_user_role_set`).
159
+ */
160
+ async createStaffAuthUser(input) {
161
+ const { data, error } = await this.supabase.auth.admin.createUser({
162
+ email: input.email,
163
+ password: input.password,
164
+ email_confirm: true,
165
+ role: 'staff',
166
+ });
167
+ if (error)
168
+ throw (0, errors_1.mapAuthError)(error);
169
+ return { id: data.user.id };
170
+ }
171
+ /**
172
+ * Bans or unbans an auth user, so a deactivated user or staff member cannot sign in or refresh.
173
+ * A ban has no "forever" in GoTrue; a hundred years is the usual stand-in.
174
+ */
175
+ async setAuthUserBanned(userId, banned) {
176
+ const { error } = await this.supabase.auth.admin.updateUserById(userId, {
177
+ ban_duration: banned ? '876000h' : 'none',
178
+ });
179
+ if (error)
180
+ throw (0, errors_1.mapAuthError)(error);
128
181
  }
129
182
  /**
130
183
  * Scope to one organization once it is known. `actorMmsId` stays `null` for system work -
@@ -0,0 +1,14 @@
1
+ import type { CreateStaffDto as CreateStaffDtoType, StaffIdDto as StaffIdDtoType, UpdateStaffPermissionsDto as UpdateStaffPermissionsDtoType } from '@managemint-solutions/entities/staff/dto';
2
+ import { UpdatePermissionsDto } from '../permissions/dto';
3
+ export declare class StaffIdDto implements StaffIdDtoType {
4
+ readonly staffId: string;
5
+ }
6
+ export declare class CreateStaffDto implements CreateStaffDtoType {
7
+ readonly email: string;
8
+ readonly name: string;
9
+ readonly password: string;
10
+ }
11
+ export declare class UpdateStaffPermissionsDto extends UpdatePermissionsDto implements UpdateStaffPermissionsDtoType {
12
+ readonly is_admin: boolean;
13
+ readonly view_dashboard: boolean;
14
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.UpdateStaffPermissionsDto = exports.CreateStaffDto = exports.StaffIdDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ const dto_1 = require("../permissions/dto");
15
+ const validators_1 = require("../validators");
16
+ class StaffIdDto {
17
+ // v4: a staff member's `mms_id` is their GoTrue auth uid, as a user's is (see UserIdDto).
18
+ staffId;
19
+ }
20
+ exports.StaffIdDto = StaffIdDto;
21
+ __decorate([
22
+ (0, class_validator_1.IsUUID)('4', { message: 'Invalid staff ID format' }),
23
+ __metadata("design:type", String)
24
+ ], StaffIdDto.prototype, "staffId", void 0);
25
+ // The email and password rules are AddUserDto's: a staff member is a GoTrue user like any other.
26
+ class CreateStaffDto {
27
+ email;
28
+ name;
29
+ password;
30
+ }
31
+ exports.CreateStaffDto = CreateStaffDto;
32
+ __decorate([
33
+ (0, class_validator_1.IsEmail)({}, { message: 'Invalid email address' }),
34
+ __metadata("design:type", String)
35
+ ], CreateStaffDto.prototype, "email", void 0);
36
+ __decorate([
37
+ (0, class_validator_1.IsString)({ message: 'Name is required' }),
38
+ (0, class_validator_1.MinLength)(1, { message: 'Name is required' }),
39
+ (0, class_validator_1.MaxLength)(100, { message: 'Name cannot be longer than 100 characters' }),
40
+ __metadata("design:type", String)
41
+ ], CreateStaffDto.prototype, "name", void 0);
42
+ __decorate([
43
+ (0, class_validator_1.IsString)({ message: 'Password is required' }),
44
+ (0, class_validator_1.MinLength)(8, { message: 'Password must be at least 8 characters' }),
45
+ (0, validators_1.MatchesPasswordRules)(),
46
+ __metadata("design:type", String)
47
+ ], CreateStaffDto.prototype, "password", void 0);
48
+ // The tenant flag set plus the two columns a staff row carries that it leaves out: admin, which
49
+ // staff grant through the api (the service refuses self), and the dashboard flag, so a row read
50
+ // back from GET can be sent as is. There is no owner.
51
+ class UpdateStaffPermissionsDto extends dto_1.UpdatePermissionsDto {
52
+ is_admin;
53
+ view_dashboard;
54
+ }
55
+ exports.UpdateStaffPermissionsDto = UpdateStaffPermissionsDto;
56
+ __decorate([
57
+ (0, class_validator_1.IsBoolean)({ message: 'Admin must be a boolean' }),
58
+ __metadata("design:type", Boolean)
59
+ ], UpdateStaffPermissionsDto.prototype, "is_admin", void 0);
60
+ __decorate([
61
+ (0, class_validator_1.IsBoolean)({ message: 'View Dashboard must be a boolean' }),
62
+ __metadata("design:type", Boolean)
63
+ ], UpdateStaffPermissionsDto.prototype, "view_dashboard", void 0);
@@ -0,0 +1,50 @@
1
+ import type { StaffEntity, StaffPermissionsEntity } from '@managemint-solutions/entities/staff';
2
+ import type { UpdateStaffPermissionsDto } from '@managemint-solutions/entities/staff/dto';
3
+ import type { TypedSupabaseClient } from '../client';
4
+ export * from './dto';
5
+ /** Admin bypasses every individual flag, so the guard reads both at once. */
6
+ type StaffPermissionFlags = {
7
+ is_admin: boolean;
8
+ } & Record<string, boolean>;
9
+ /**
10
+ * The `staff` table. Staff read it on their own client; every write is the service role's (staff
11
+ * hold SELECT only), so `preRegister`, `removePending` and `setActive` belong to the service
12
+ * client. Unaudited: an `audit_logs` row belongs to an organization, and staff have none.
13
+ */
14
+ export declare class StaffResource {
15
+ private readonly supabase;
16
+ constructor(supabase: TypedSupabaseClient);
17
+ /** Every staff member, by name. A pending pre-registration is not one yet. */
18
+ list(): Promise<StaffEntity[]>;
19
+ getById(staffId: string): Promise<StaffEntity>;
20
+ /**
21
+ * The address, written before its auth user exists - the first half of creating a staff
22
+ * member. `mms_id` stays null until GoTrue creates the auth user with the `staff` role and
23
+ * `handle_auth_user_role_set` links the two.
24
+ */
25
+ preRegister(input: {
26
+ email: string;
27
+ name: string;
28
+ created_by: string;
29
+ }): Promise<void>;
30
+ /** Undoes a pre-registration whose auth user was never created. A linked row is never touched. */
31
+ removePending(email: string): Promise<void>;
32
+ /** Deactivates, stamping who and when, or reactivates, clearing them. */
33
+ setActive(staffId: string, active: boolean, by: string): Promise<StaffEntity>;
34
+ }
35
+ /**
36
+ * The `staff_permissions` table: one row per staff member, made by the role trigger alongside
37
+ * the link. Staff read it on their own client; `updateForStaff` is the service client's.
38
+ */
39
+ export declare class StaffPermissionsResource {
40
+ private readonly supabase;
41
+ constructor(supabase: TypedSupabaseClient);
42
+ /**
43
+ * The one lookup the admin api's guard makes per gated request: the flag the route asked for,
44
+ * plus the admin bypass. Null when there is no row, which the guard treats as a denial.
45
+ */
46
+ flagsFor(staffId: string, flag: string): Promise<StaffPermissionFlags | null>;
47
+ /** The whole row. `*`: every flag, so a new flag column needs no select edit. */
48
+ getForStaff(staffId: string): Promise<StaffPermissionsEntity>;
49
+ updateForStaff(staffId: string, patch: UpdateStaffPermissionsDto, by: string): Promise<StaffPermissionsEntity>;
50
+ }