@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
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.StaffPermissionsResource = exports.StaffResource = void 0;
18
+ const audit_1 = require("../audit");
19
+ const errors_1 = require("../errors");
20
+ const permissions_1 = require("../permissions");
21
+ __exportStar(require("./dto"), exports);
22
+ const STAFF_SELECT = 'mms_id, email, name, active, created_at, created_by, deactivated_at, deactivated_by';
23
+ /**
24
+ * The `staff` table. Staff read it on their own client; every write is the service role's (staff
25
+ * hold SELECT only), so `preRegister`, `removePending` and `setActive` belong to the service
26
+ * client. Unaudited: an `audit_logs` row belongs to an organization, and staff have none.
27
+ */
28
+ class StaffResource {
29
+ supabase;
30
+ constructor(supabase) {
31
+ this.supabase = supabase;
32
+ }
33
+ /** Every staff member, by name. A pending pre-registration is not one yet. */
34
+ async list() {
35
+ const { data, error } = await this.supabase
36
+ .from('staff')
37
+ .select(STAFF_SELECT)
38
+ .not('mms_id', 'is', null)
39
+ .order('name');
40
+ if (error)
41
+ throw (0, errors_1.mapPostgrestError)(error);
42
+ return (data ?? []);
43
+ }
44
+ async getById(staffId) {
45
+ const { data, error } = await this.supabase
46
+ .from('staff')
47
+ .select(STAFF_SELECT)
48
+ .eq('mms_id', staffId)
49
+ .single();
50
+ if (error)
51
+ throw (0, errors_1.mapPostgrestError)(error);
52
+ return data;
53
+ }
54
+ /**
55
+ * The address, written before its auth user exists - the first half of creating a staff
56
+ * member. `mms_id` stays null until GoTrue creates the auth user with the `staff` role and
57
+ * `handle_auth_user_role_set` links the two.
58
+ */
59
+ async preRegister(input) {
60
+ const { error } = await this.supabase.from('staff').insert(input);
61
+ if (error)
62
+ throw (0, errors_1.mapPostgrestError)(error);
63
+ }
64
+ /** Undoes a pre-registration whose auth user was never created. A linked row is never touched. */
65
+ async removePending(email) {
66
+ const { error } = await this.supabase
67
+ .from('staff')
68
+ .delete()
69
+ .eq('email', email)
70
+ .is('mms_id', null);
71
+ if (error)
72
+ throw (0, errors_1.mapPostgrestError)(error);
73
+ }
74
+ /** Deactivates, stamping who and when, or reactivates, clearing them. */
75
+ async setActive(staffId, active, by) {
76
+ const { data, error } = await this.supabase
77
+ .from('staff')
78
+ .update({
79
+ active,
80
+ deactivated_at: active ? null : new Date().toISOString(),
81
+ deactivated_by: active ? null : by,
82
+ })
83
+ .eq('mms_id', staffId)
84
+ .select(STAFF_SELECT)
85
+ .single();
86
+ if (error)
87
+ throw (0, errors_1.mapPostgrestError)(error);
88
+ return data;
89
+ }
90
+ }
91
+ exports.StaffResource = StaffResource;
92
+ /**
93
+ * The `staff_permissions` table: one row per staff member, made by the role trigger alongside
94
+ * the link. Staff read it on their own client; `updateForStaff` is the service client's.
95
+ */
96
+ class StaffPermissionsResource {
97
+ supabase;
98
+ constructor(supabase) {
99
+ this.supabase = supabase;
100
+ }
101
+ /**
102
+ * The one lookup the admin api's guard makes per gated request: the flag the route asked for,
103
+ * plus the admin bypass. Null when there is no row, which the guard treats as a denial.
104
+ */
105
+ async flagsFor(staffId, flag) {
106
+ (0, permissions_1.checkPermissionFlag)(flag);
107
+ const { data, error } = await this.supabase
108
+ .from('staff_permissions')
109
+ .select(`is_admin, ${flag}`)
110
+ .eq('staff_id', staffId)
111
+ .maybeSingle();
112
+ if (error)
113
+ throw (0, errors_1.mapPostgrestError)(error);
114
+ return (data ?? null);
115
+ }
116
+ /** The whole row. `*`: every flag, so a new flag column needs no select edit. */
117
+ async getForStaff(staffId) {
118
+ const { data, error } = await this.supabase
119
+ .from('staff_permissions')
120
+ .select('*')
121
+ .eq('staff_id', staffId)
122
+ .single();
123
+ if (error)
124
+ throw (0, errors_1.mapPostgrestError)(error);
125
+ return (0, audit_1.withoutInternalId)(data);
126
+ }
127
+ async updateForStaff(staffId, patch, by) {
128
+ const { data, error } = await this.supabase
129
+ .from('staff_permissions')
130
+ .update({ ...patch, updated_at: new Date().toISOString(), last_updated_by: by })
131
+ .eq('staff_id', staffId)
132
+ .select('*')
133
+ .single();
134
+ if (error)
135
+ throw (0, errors_1.mapPostgrestError)(error);
136
+ return (0, audit_1.withoutInternalId)(data);
137
+ }
138
+ }
139
+ exports.StaffPermissionsResource = StaffPermissionsResource;
@@ -0,0 +1,37 @@
1
+ import { type StaffContext } from './auth';
2
+ import { AuditResource } from './audit';
3
+ import type { SupabaseClientConfig, TypedSupabaseClient } from './client';
4
+ import { ClientsResource } from './clients';
5
+ import { LeadsResource } from './leads';
6
+ import { OrganizationsResource } from './organizations';
7
+ import { ProjectsResource } from './projects';
8
+ import { StaffPermissionsResource, StaffResource } from './staff';
9
+ import { TasksResource } from './tasks';
10
+ import { UserApiCredentialsReads } from './user-api-credentials';
11
+ import { UsersResource } from './users';
12
+ type SupabaseStaffClientConfig = Omit<SupabaseClientConfig, 'auth' | 'onAuditError'> & {
13
+ auth: StaffContext;
14
+ };
15
+ /**
16
+ * A staff member's data client, for the admin api: every query carries the staff token, so the
17
+ * `staff` role's policies decide what comes back. Reads span every organization and every write
18
+ * is refused with a 403. A resource not listed here is simply not available to staff.
19
+ */
20
+ export declare class SupabaseStaffClient {
21
+ readonly supabase: TypedSupabaseClient;
22
+ /** The staff member the token belongs to - the caller, for a service that records who acted. */
23
+ readonly auth: StaffContext;
24
+ readonly audit: AuditResource;
25
+ readonly clients: ClientsResource;
26
+ readonly leads: LeadsResource;
27
+ readonly projects: ProjectsResource;
28
+ readonly tasks: TasksResource;
29
+ readonly users: UsersResource;
30
+ readonly organizations: OrganizationsResource;
31
+ readonly staff: StaffResource;
32
+ readonly staffPermissions: StaffPermissionsResource;
33
+ readonly userApiCredentials: UserApiCredentialsReads;
34
+ constructor({ url, key, accessToken, auth, client }: SupabaseStaffClientConfig);
35
+ }
36
+ export declare const createSupabaseStaffClient: (config: SupabaseStaffClientConfig) => SupabaseStaffClient;
37
+ export {};
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSupabaseStaffClient = exports.SupabaseStaffClient = void 0;
4
+ const auth_1 = require("./auth");
5
+ const audit_1 = require("./audit");
6
+ const additional_details_1 = require("./additional-details");
7
+ const clients_1 = require("./clients");
8
+ const leads_1 = require("./leads");
9
+ const organizations_1 = require("./organizations");
10
+ const projects_1 = require("./projects");
11
+ const staff_1 = require("./staff");
12
+ const tasks_1 = require("./tasks");
13
+ const user_api_credentials_1 = require("./user-api-credentials");
14
+ const users_1 = require("./users");
15
+ /**
16
+ * A staff member's data client, for the admin api: every query carries the staff token, so the
17
+ * `staff` role's policies decide what comes back. Reads span every organization and every write
18
+ * is refused with a 403. A resource not listed here is simply not available to staff.
19
+ */
20
+ class SupabaseStaffClient {
21
+ supabase; // escape hatch for not-yet-migrated queries
22
+ /** The staff member the token belongs to - the caller, for a service that records who acted. */
23
+ auth;
24
+ audit;
25
+ clients;
26
+ leads;
27
+ projects;
28
+ tasks;
29
+ users;
30
+ organizations;
31
+ staff;
32
+ staffPermissions;
33
+ userApiCredentials;
34
+ constructor({ url, key, accessToken, auth, client }) {
35
+ this.supabase = client ?? (0, auth_1.createRawClient)(url, key, accessToken);
36
+ this.auth = auth;
37
+ this.audit = new audit_1.AuditResource(this.supabase, auth);
38
+ this.clients = new clients_1.ClientsResource(this.supabase, auth, this.audit);
39
+ const additionalDetails = new additional_details_1.AdditionalDetailsResource(this.supabase, auth, this.audit);
40
+ this.leads = new leads_1.LeadsResource(this.supabase, auth, this.audit, additionalDetails);
41
+ this.projects = new projects_1.ProjectsResource(this.supabase, auth, this.audit);
42
+ this.tasks = new tasks_1.TasksResource(this.supabase, auth, this.audit);
43
+ this.users = new users_1.UsersResource(this.supabase, auth, this.audit);
44
+ this.organizations = new organizations_1.OrganizationsResource(this.supabase, auth, this.audit);
45
+ this.staff = new staff_1.StaffResource(this.supabase);
46
+ this.staffPermissions = new staff_1.StaffPermissionsResource(this.supabase);
47
+ this.userApiCredentials = new user_api_credentials_1.UserApiCredentialsReads(this.supabase);
48
+ }
49
+ }
50
+ exports.SupabaseStaffClient = SupabaseStaffClient;
51
+ const createSupabaseStaffClient = (config) => new SupabaseStaffClient(config);
52
+ exports.createSupabaseStaffClient = createSupabaseStaffClient;
@@ -1,4 +1,4 @@
1
- import type { AddTaskLinkDto as AddTaskLinkDtoType, CreateTaskDto as CreateTaskDtoType, GetTasksDto as GetTasksDtoType, TaskIdDto as TaskIdDtoType, TaskLinkIdDto as TaskLinkIdDtoType, UpdateTaskDto as UpdateTaskDtoType } from '@managemint-solutions/entities/tasks/dto';
1
+ import type { AddTaskLinkDto as AddTaskLinkDtoType, CreateTaskDto as CreateTaskDtoType, GetTasksDto as GetTasksDtoType, StaffGetTasksDto as StaffGetTasksDtoType, TaskIdDto as TaskIdDtoType, TaskLinkIdDto as TaskLinkIdDtoType, UpdateTaskDto as UpdateTaskDtoType } from '@managemint-solutions/entities/tasks/dto';
2
2
  import { TaskLinkType } from '@managemint-solutions/entities/tasks/enum';
3
3
  export declare class TaskIdDto implements TaskIdDtoType {
4
4
  readonly taskId: string;
@@ -31,6 +31,10 @@ export declare class GetTasksDto implements Omit<GetTasksDtoType, 'status_id_in'
31
31
  readonly priority_in?: number[] | null;
32
32
  readonly archive: boolean;
33
33
  }
34
+ /** A staff read across organizations, optionally narrowed to one of them. */
35
+ export declare class StaffGetTasksDto extends GetTasksDto implements Omit<StaffGetTasksDtoType, 'status_id_in' | 'status_id_not_in' | 'label_id_in' | 'priority_in'> {
36
+ readonly organization_id?: string | null;
37
+ }
34
38
  export declare class CreateTaskDto implements CreateTaskDtoType {
35
39
  readonly name: string;
36
40
  readonly description?: string | null;
package/dist/tasks/dto.js CHANGED
@@ -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.UpdateTaskDto = exports.CreateTaskDto = exports.GetTasksDto = exports.AddTaskLinkDto = exports.TaskLinkIdDto = exports.TaskIdDto = void 0;
12
+ exports.UpdateTaskDto = exports.CreateTaskDto = exports.StaffGetTasksDto = exports.GetTasksDto = exports.AddTaskLinkDto = exports.TaskLinkIdDto = exports.TaskIdDto = void 0;
13
13
  const class_validator_1 = require("class-validator");
14
14
  const class_transformer_1 = require("class-transformer");
15
15
  const enum_1 = require("@managemint-solutions/entities/tasks/enum");
@@ -206,6 +206,17 @@ __decorate([
206
206
  (0, class_validator_1.IsBoolean)({ message: 'Include archived must be a boolean' }),
207
207
  __metadata("design:type", Boolean)
208
208
  ], GetTasksDto.prototype, "archive", void 0);
209
+ /** A staff read across organizations, optionally narrowed to one of them. */
210
+ class StaffGetTasksDto extends GetTasksDto {
211
+ organization_id;
212
+ }
213
+ exports.StaffGetTasksDto = StaffGetTasksDto;
214
+ __decorate([
215
+ (0, class_validator_1.IsOptional)(),
216
+ (0, validators_1.IsNullable)(),
217
+ (0, class_validator_1.IsUUID)('7', { message: 'Invalid organization ID format' }),
218
+ __metadata("design:type", Object)
219
+ ], StaffGetTasksDto.prototype, "organization_id", void 0);
209
220
  class CreateTaskDto {
210
221
  name;
211
222
  description;
@@ -1,6 +1,6 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { TaskEntity, TaskSummary } from '@managemint-solutions/entities/tasks';
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 { AddTaskLinkDto, CreateTaskDto, GetTasksDto, UpdateTaskDto } from './dto';
@@ -12,8 +12,10 @@ export declare class TasksResource {
12
12
  private readonly table;
13
13
  /** This record's history - its own rows and the rows that hang off it. */
14
14
  readonly audit: EntityAuditTrail;
15
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, auditLog: AuditResource);
16
- list(query: GetTasksDto): Promise<MmsList<TaskSummary>>;
15
+ constructor(supabase: TypedSupabaseClient, auth: ClientContext, auditLog: AuditResource);
16
+ list(query: GetTasksDto & {
17
+ organization_id?: string | null;
18
+ }): Promise<MmsList<TaskSummary>>;
17
19
  getById(taskId: string): Promise<TaskEntity>;
18
20
  create(input: CreateTaskDto): Promise<TaskEntity>;
19
21
  update(taskId: string, input: UpdateTaskDto): Promise<TaskEntity>;
@@ -20,6 +20,7 @@ const enum_2 = require("@managemint-solutions/entities/labels/enum");
20
20
  const enum_3 = require("@managemint-solutions/entities/audit-trail/enum");
21
21
  const audit_1 = require("../audit");
22
22
  const errors_1 = require("../errors");
23
+ const tenancy_1 = require("../tenancy");
23
24
  const transforms_1 = require("../transforms");
24
25
  __exportStar(require("./dto"), exports);
25
26
  // The public contract is the shared TaskSummary / TaskEntity from @managemint-solutions/entities;
@@ -146,17 +147,17 @@ class TasksResource {
146
147
  const labelIdIn = query.label_id_in && query.label_id_in.length > 0 ? query.label_id_in : null;
147
148
  // `order`/`range` return `this`, so the builder stays a filter builder and the conditional
148
149
  // filters below can be reassigned onto it without a cast.
149
- let tasksQuery = this.supabase
150
+ let tasksQuery = (0, tenancy_1.scopeToOrganization)(this.supabase
150
151
  .from('tasks')
151
- .select(labelIdIn ? `${TASK_LIST_SELECT}, ${LABEL_FILTER_SELECT}` : TASK_LIST_SELECT, {
152
- count: 'estimated',
153
- })
154
- .eq('organization_id', this.auth.organization_id)
152
+ .select((0, tenancy_1.withOrganization)(labelIdIn ? `${TASK_LIST_SELECT}, ${LABEL_FILTER_SELECT}` : TASK_LIST_SELECT, this.auth), { count: 'estimated' }), this.auth)
155
153
  .eq('archived', query.archive)
156
154
  .eq('deleted', false)
157
155
  .order('priority', { ascending: false })
158
156
  .order('created_at', { ascending: false })
159
157
  .range(offset, offset + pageSize - 1);
158
+ if (query.organization_id) {
159
+ tasksQuery = tasksQuery.eq('organization_id', query.organization_id);
160
+ }
160
161
  if (trimmedSearch) {
161
162
  // "TASK-12", "#12" or "12" also finds task number 12; anything else is a text search only.
162
163
  const taskNumber = trimmedSearch.match(/^(?:[a-z]+-|#)?(\d+)$/i)?.[1];
@@ -220,11 +221,7 @@ class TasksResource {
220
221
  return (0, transforms_1.toMmsList)(rows, page, pageSize, count);
221
222
  }
222
223
  async getById(taskId) {
223
- const { data, error } = await this.supabase
224
- .from('tasks')
225
- .select(TASK_DETAIL_SELECT)
226
- .eq('mms_id', taskId)
227
- .eq('organization_id', this.auth.organization_id)
224
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('tasks').select((0, tenancy_1.withOrganization)(TASK_DETAIL_SELECT, this.auth)).eq('mms_id', taskId), this.auth)
228
225
  .order('created_at', { referencedTable: 'links_out' })
229
226
  .order('created_at', { referencedTable: 'links_in' })
230
227
  .single();
@@ -250,7 +247,7 @@ class TasksResource {
250
247
  // BEFORE INSERT trigger fills it in when it arrives null.
251
248
  const row = await this.table.insert({
252
249
  ...fields,
253
- organization_id: this.auth.organization_id,
250
+ organization_id: (0, tenancy_1.requireOrganization)(this.auth),
254
251
  created_by: this.auth.mms_id,
255
252
  created_at: new Date().toISOString(),
256
253
  });
@@ -300,6 +297,7 @@ class TasksResource {
300
297
  * made from.
301
298
  */
302
299
  async linkTask(taskId, input) {
300
+ const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
303
301
  const refuse = (status, message, cause) => new errors_1.SupabaseClientError({ status, error: 'Database Error', message }, { cause });
304
302
  if (input.linked_task_id === taskId) {
305
303
  throw refuse(400, 'A task cannot be linked to itself');
@@ -307,7 +305,7 @@ class TasksResource {
307
305
  const { data: tasks, error: tasksError } = await this.supabase
308
306
  .from('tasks')
309
307
  .select('mms_id, task_number:task_id')
310
- .eq('organization_id', this.auth.organization_id)
308
+ .eq('organization_id', organizationId)
311
309
  .eq('deleted', false)
312
310
  .in('mms_id', [taskId, input.linked_task_id]);
313
311
  if (tasksError)
@@ -328,7 +326,7 @@ class TasksResource {
328
326
  const { data: parentLink, error: parentError } = await this.supabase
329
327
  .from('task_links')
330
328
  .select('task_id')
331
- .eq('organization_id', this.auth.organization_id)
329
+ .eq('organization_id', organizationId)
332
330
  .eq('linked_task_id', child)
333
331
  .eq('link_type', enum_1.TaskLinkType.CHILD)
334
332
  .maybeSingle();
@@ -344,7 +342,7 @@ class TasksResource {
344
342
  task_id: taskId,
345
343
  linked_task_id: input.linked_task_id,
346
344
  link_type: input.link_type,
347
- organization_id: this.auth.organization_id,
345
+ organization_id: organizationId,
348
346
  created_by: this.auth.mms_id,
349
347
  });
350
348
  if (error) {
@@ -376,7 +374,7 @@ class TasksResource {
376
374
  const { data, error } = await this.supabase
377
375
  .from('task_links')
378
376
  .delete()
379
- .eq('organization_id', this.auth.organization_id)
377
+ .eq('organization_id', (0, tenancy_1.requireOrganization)(this.auth))
380
378
  .or(`and(task_id.eq.${taskId},linked_task_id.eq.${linkedTaskId}),and(task_id.eq.${linkedTaskId},linked_task_id.eq.${taskId})`)
381
379
  .select('task_id, link_type, from:tasks!task_id ( task_number:task_id ), to:tasks!linked_task_id ( task_number:task_id )');
382
380
  if (error)
@@ -409,7 +407,7 @@ class TasksResource {
409
407
  const { count, error } = await this.supabase
410
408
  .from('labels')
411
409
  .select('mms_id', { count: 'exact', head: true })
412
- .eq('organization_id', this.auth.organization_id)
410
+ .eq('organization_id', (0, tenancy_1.requireOrganization)(this.auth))
413
411
  .eq('entity_type', enum_2.LabelEntityTypes.TASK)
414
412
  .in('mms_id', labelIds);
415
413
  if (error)
@@ -432,11 +430,12 @@ class TasksResource {
432
430
  * across an edit keeps its place.
433
431
  */
434
432
  async setLabels(taskId, labelIds) {
433
+ const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
435
434
  const { data: current, error: readError } = await this.supabase
436
435
  .from('task_labels')
437
436
  .select(PINNED_LABEL_SELECT)
438
437
  .eq('task_id', taskId)
439
- .eq('organization_id', this.auth.organization_id)
438
+ .eq('organization_id', organizationId)
440
439
  .order('created_at');
441
440
  if (readError)
442
441
  throw (0, errors_1.mapPostgrestError)(readError);
@@ -453,7 +452,7 @@ class TasksResource {
453
452
  .insert(added.map((labelId) => ({
454
453
  task_id: taskId,
455
454
  label_id: labelId,
456
- organization_id: this.auth.organization_id,
455
+ organization_id: organizationId,
457
456
  created_by: this.auth.mms_id,
458
457
  })))
459
458
  .select(PINNED_LABEL_SELECT);
@@ -466,7 +465,7 @@ class TasksResource {
466
465
  .from('task_labels')
467
466
  .delete()
468
467
  .eq('task_id', taskId)
469
- .eq('organization_id', this.auth.organization_id)
468
+ .eq('organization_id', organizationId)
470
469
  .in('label_id', dropped.map((row) => row.label_id));
471
470
  if (error)
472
471
  throw (0, errors_1.mapPostgrestError)(error);
@@ -0,0 +1,13 @@
1
+ import type { PostgrestFilterBuilder } from '@supabase/postgrest-js';
2
+ /** A read filtered to the caller's organization, or left across every organization for staff. */
3
+ export declare const scopeToOrganization: <Q extends PostgrestFilterBuilder<any, any, any, any, any, any, any>>(query: Q, actor: {
4
+ organization_id: string | null;
5
+ }) => Q;
6
+ /** The caller's organization id, for a write; staff have none and are refused with a 403. */
7
+ export declare const requireOrganization: (actor: {
8
+ organization_id: string | null;
9
+ }) => string;
10
+ /** A select, plus the row's organization on a staff read so the row says whose it is. */
11
+ export declare const withOrganization: (select: string, actor: {
12
+ organization_id: string | null;
13
+ }) => string;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withOrganization = exports.requireOrganization = exports.scopeToOrganization = void 0;
4
+ const errors_1 = require("./errors");
5
+ // How a resource scopes itself to the caller's organization. A tenant (`AuthContext`) reads and
6
+ // writes inside their own; staff (`StaffContext`, `organization_id: null`) read across every
7
+ // organization, as their role's policies allow, and write nothing.
8
+ /** A read filtered to the caller's organization, or left across every organization for staff. */
9
+ const scopeToOrganization = (query, actor) => (actor.organization_id === null ? query : query.eq('organization_id', actor.organization_id));
10
+ exports.scopeToOrganization = scopeToOrganization;
11
+ /** The caller's organization id, for a write; staff have none and are refused with a 403. */
12
+ const requireOrganization = (actor) => {
13
+ if (actor.organization_id === null) {
14
+ throw new errors_1.SupabaseClientError({
15
+ status: 403,
16
+ error: 'Permission Error',
17
+ message: 'Organization required',
18
+ });
19
+ }
20
+ return actor.organization_id;
21
+ };
22
+ exports.requireOrganization = requireOrganization;
23
+ /** A select, plus the row's organization on a staff read so the row says whose it is. */
24
+ const withOrganization = (select, actor) => actor.organization_id === null ? `${select}, organization:organizations!organization_id(mms_id, name)` : select;
25
+ exports.withOrganization = withOrganization;
@@ -0,0 +1,12 @@
1
+ import type { CreateUserApiCredentialDto as CreateUserApiCredentialDtoType, MintEmulationTokenDto as MintEmulationTokenDtoType, UserApiCredentialIdDto as UserApiCredentialIdDtoType } from '@managemint-solutions/entities/user-api-credentials/dto';
2
+ export declare class UserApiCredentialIdDto implements UserApiCredentialIdDtoType {
3
+ readonly credentialId: string;
4
+ }
5
+ export declare class CreateUserApiCredentialDto implements CreateUserApiCredentialDtoType {
6
+ readonly label: string;
7
+ readonly expires_at?: string | null;
8
+ }
9
+ export declare class MintEmulationTokenDto implements MintEmulationTokenDtoType {
10
+ readonly client_id: string;
11
+ readonly client_secret: string;
12
+ }
@@ -0,0 +1,56 @@
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.MintEmulationTokenDto = exports.CreateUserApiCredentialDto = exports.UserApiCredentialIdDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ const validators_1 = require("../validators");
15
+ class UserApiCredentialIdDto {
16
+ credentialId;
17
+ }
18
+ exports.UserApiCredentialIdDto = UserApiCredentialIdDto;
19
+ __decorate([
20
+ (0, class_validator_1.IsUUID)('7', { message: 'Invalid credential ID format' }),
21
+ __metadata("design:type", String)
22
+ ], UserApiCredentialIdDto.prototype, "credentialId", void 0);
23
+ class CreateUserApiCredentialDto {
24
+ label;
25
+ expires_at;
26
+ }
27
+ exports.CreateUserApiCredentialDto = CreateUserApiCredentialDto;
28
+ __decorate([
29
+ (0, class_validator_1.IsString)({ message: 'Label is required' }),
30
+ (0, class_validator_1.MinLength)(1, { message: 'Label is required' }),
31
+ (0, class_validator_1.MaxLength)(100, { message: 'Label cannot be longer than 100 characters' }),
32
+ __metadata("design:type", String)
33
+ ], CreateUserApiCredentialDto.prototype, "label", void 0);
34
+ __decorate([
35
+ (0, class_validator_1.IsOptional)(),
36
+ (0, validators_1.IsNullable)(),
37
+ (0, class_validator_1.IsDateString)({}, { message: 'Expiry must be a valid ISO 8601 date string' }),
38
+ __metadata("design:type", Object)
39
+ ], CreateUserApiCredentialDto.prototype, "expires_at", void 0);
40
+ class MintEmulationTokenDto {
41
+ client_id;
42
+ client_secret;
43
+ }
44
+ exports.MintEmulationTokenDto = MintEmulationTokenDto;
45
+ __decorate([
46
+ (0, class_validator_1.IsString)({ message: 'Client ID is required' }),
47
+ (0, class_validator_1.MinLength)(1, { message: 'Client ID is required' }),
48
+ (0, class_validator_1.MaxLength)(255, { message: 'Client ID cannot be longer than 255 characters' }),
49
+ __metadata("design:type", String)
50
+ ], MintEmulationTokenDto.prototype, "client_id", void 0);
51
+ __decorate([
52
+ (0, class_validator_1.IsString)({ message: 'Client secret is required' }),
53
+ (0, class_validator_1.MinLength)(1, { message: 'Client secret is required' }),
54
+ (0, class_validator_1.MaxLength)(255, { message: 'Client secret cannot be longer than 255 characters' }),
55
+ __metadata("design:type", String)
56
+ ], MintEmulationTokenDto.prototype, "client_secret", void 0);
@@ -0,0 +1,47 @@
1
+ import type { UserApiCredentialEntity } from '@managemint-solutions/entities/user-api-credentials';
2
+ import { type OrganizationTableFactory } from '../audit';
3
+ import type { TypedSupabaseClient } from '../client';
4
+ export * from './dto';
5
+ /** What `create` writes: the secret already hashed, and the staff member who issued it. */
6
+ export type CreateUserApiCredentialRow = {
7
+ user_id: string;
8
+ organization_id: string;
9
+ client_id: string;
10
+ secret_hash: string;
11
+ label: string;
12
+ expires_at: string | null;
13
+ created_by: string;
14
+ };
15
+ /** The whole row, the secret hash included - what a mint is checked against. */
16
+ export type UserApiCredentialRow = UserApiCredentialEntity & {
17
+ secret_hash: string;
18
+ };
19
+ /**
20
+ * Credentials as staff read them, on their own client, through the column grant that leaves the
21
+ * secret hash out. Tenant users have no grant at all.
22
+ */
23
+ export declare class UserApiCredentialsReads {
24
+ private readonly supabase;
25
+ constructor(supabase: TypedSupabaseClient);
26
+ /** A user's credentials, newest first. */
27
+ listForUser(userId: string): Promise<UserApiCredentialEntity[]>;
28
+ }
29
+ /**
30
+ * The writes and the secret check, on the service client only: every write is the service
31
+ * role's, and nobody else may read the hash. A credential's writes are audited against its own
32
+ * organization as the system - the staff member who issued or revoked it is on the row itself
33
+ * (`created_by`, `revoked_by`), and is not a user an audit row can name.
34
+ */
35
+ export declare class UserApiCredentialsResource {
36
+ private readonly supabase;
37
+ private readonly tableFor;
38
+ constructor(supabase: TypedSupabaseClient, tableFor: OrganizationTableFactory);
39
+ /** Issues a credential. Its `secret_hash` is redacted in the audit row and never returned. */
40
+ create(row: CreateUserApiCredentialRow): Promise<UserApiCredentialEntity>;
41
+ /** The credential a client id names, with its secret hash. Null when none does. */
42
+ findByClientId(clientId: string): Promise<UserApiCredentialRow | null>;
43
+ /** Revokes a live credential. Null when it was revoked already. */
44
+ revoke(credentialId: string, revokedBy: string): Promise<UserApiCredentialEntity | null>;
45
+ /** Stamps a successful mint, unaudited: when it was last used, not a change to the credential. */
46
+ markUsed(credentialId: string): Promise<void>;
47
+ }