@rytass/bpm-core-nestjs-module 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
  Releases are managed by [`nx release`](https://nx.dev/recipes/nx-release) with
10
10
  Conventional Commits — see `nx.json` for the release config.
11
11
 
12
+ ## 0.1.7 — 2026-05-28
13
+
14
+ ### Documentation
15
+
16
+ - **README "Bring-your-own-host-auth" recipe** added under Auth
17
+ Context. Documents the `ApiMember` JSON shape, the expected status
18
+ codes for `/auth/login`, `/auth/me`, `/auth/logout`, and two
19
+ integration patterns: (A) extend the host API with `/auth/*`
20
+ controllers that emit `ApiMember` (recommended for member-base
21
+ hosts), or (B) run a thin wrapper host on a separate subdomain.
22
+ - **README "Sharing a Postgres cluster with the host"** added under
23
+ Database Setup. Compares same-schema / separate-schema / separate-DB
24
+ trade-offs, recommends separate-schema, clarifies Vault path
25
+ isolation between host and BPM.
26
+ - **`commitOrgUnitTreeDraft` signature corrected** in the Worked
27
+ Example listing — previously `{ baseUpdatedAt, draft }` (incorrect),
28
+ now `{ moves: { id, parentId, baseUpdatedAt }[] }` matching the
29
+ actual export.
30
+ - **`OrgUnitType` literal case corrected** in the README's example
31
+ comment — previously `'COMPANY' | 'DIVISION' | ...`, now `'company'
32
+ | 'division' | ...`. (See `@rytass/bpm-core-shared@0.1.7` for the
33
+ actual TS type change.)
34
+ - **"Positions are intentionally not deletable" note** added to the
35
+ Organization mirror pattern. Explains why no `deletePosition`
36
+ mutation exists and how to retire a position safely.
37
+ - **README "Package Status" current-version line refreshed** to
38
+ `0.1.7`.
39
+
40
+ ### Why a patch
41
+
42
+ Documentation-only. No public API change beyond what
43
+ `@rytass/bpm-core-shared@0.1.7` ships.
44
+
45
+ ## 0.1.6 — 2026-05-28
46
+
47
+ ### Added
48
+
49
+ - **`MemberNotFoundException`** is now exported from the root barrel
50
+ (and `/identity` subpath). The `BPMMemberResolver.resolve` JSDoc has
51
+ referenced this class for two releases without actually shipping it;
52
+ hosts can now `throw new MemberNotFoundException(memberId)` directly.
53
+
54
+ ### Documentation
55
+
56
+ - **README "Mapping from a host RBAC system (e.g. Casbin)"**: worked
57
+ example showing how to project a host's grouping policy
58
+ (`enforcer.getRolesForUser(memberId)`) into BPM's exact-string role
59
+ literals inside `authContextFactory`.
60
+ - **README "Cross-origin authentication"** under Attachment Storage:
61
+ documents that signed URLs carry auth inside the URL (not via cookie),
62
+ what the TTL controls, and the production-grade defenses on top.
63
+ - **README "Disabling the `/auth/test-members` endpoint in production"**:
64
+ clarifies that the endpoint is wrapper-host-owned (BPM does not
65
+ register it) and that the React client degrades gracefully when the
66
+ endpoint is absent.
67
+ - **README Worked Example rewritten** to call the actual published
68
+ `@rytass/bpm-core-client/organization` exports
69
+ (`readOrganizationDashboard`, flat-input `createOrgUnit` /
70
+ `updateOrgUnit`) — the 0.1.5 example called functions that didn't
71
+ exist. Adds an "Atomicity caveat" note clarifying that only
72
+ `commitOrgUnitTreeDraft` is transactional; sequential
73
+ `createMembership` calls are independent.
74
+
75
+ ### Why a patch
76
+
77
+ `MemberNotFoundException` is purely additive. Other changes are docs.
78
+
12
79
  ## 0.1.5 — 2026-05-28
13
80
 
14
81
  ### Documentation
package/README.md CHANGED
@@ -14,7 +14,7 @@ directory integration, storage adapters, and deployment.
14
14
 
15
15
  ## Package Status
16
16
 
17
- Current version: `0.0.1`
17
+ Current version: `0.1.7`
18
18
 
19
19
  The package is intended for NestJS backend hosts. It does not include the
20
20
  Next.js backoffice UI and does not provide a production auth system by itself.
@@ -198,6 +198,104 @@ class HostResolver {
198
198
  }
199
199
  ```
200
200
 
201
+ ### Bring-your-own-host-auth (for hosts that already have auth)
202
+
203
+ The BPM React `<AuthProvider>` calls `POST /auth/login`, `GET /auth/me`,
204
+ and `POST /auth/logout` on the BPM API host and expects responses that
205
+ match the `ApiMember` shape (exported from `@rytass/bpm-core-client`):
206
+
207
+ ```ts
208
+ interface ApiMember {
209
+ readonly memberId: string;
210
+ readonly email: string;
211
+ readonly name: string;
212
+ readonly roles: readonly string[];
213
+ readonly permissions: readonly string[];
214
+ readonly expiresAt: string; // ISO 8601
215
+ }
216
+ ```
217
+
218
+ Status-code contract:
219
+
220
+ | Endpoint | Success | Failure |
221
+ | --------------------- | ----------- | ----------------------------------- |
222
+ | `POST /auth/login` | `200` + `ApiMember` JSON | `401` (bad credentials) / `400` (validation) |
223
+ | `GET /auth/me` | `200` + `ApiMember` JSON | `401` (no session — React client treats as anonymous, not error) |
224
+ | `POST /auth/logout` | `204` (or `200` empty) | `401` ignored |
225
+
226
+ Session lifetime is owned by the host. The React `<AuthProvider>`
227
+ relies on `credentials: 'include'` plus an HTTP-only cookie issued by
228
+ the host on successful login — BPM does **not** issue cookies itself.
229
+
230
+ Hosts that already have their own JWT / cookie auth have two integration
231
+ patterns:
232
+
233
+ #### Pattern A — Extend the host API surface
234
+
235
+ Add `/auth/login`, `/auth/me`, `/auth/logout` controllers to your
236
+ existing NestJS host. Internally those endpoints call the host's
237
+ existing auth service (e.g. `MemberBaseService.login()`) and emit the
238
+ `ApiMember` shape. This is the simplest integration when BPM and the
239
+ host run on the same origin. Example for a member-base host:
240
+
241
+ ```ts
242
+ @Controller('auth')
243
+ export class HostAuthController {
244
+ constructor(private readonly memberBase: MemberBaseService) {}
245
+
246
+ @Post('login')
247
+ async login(@Body() input: LoginInput, @Res({ passthrough: true }) res: Response): Promise<ApiMember> {
248
+ const { member, accessToken, refreshToken } = await this.memberBase.login(input);
249
+ // Set the host's existing HTTP-only cookies; the React client will
250
+ // forward them on subsequent /auth/me and /graphql calls.
251
+ res.cookie('access_token', accessToken, { httpOnly: true, sameSite: 'lax' });
252
+ res.cookie('refresh_token', refreshToken, { httpOnly: true, sameSite: 'lax' });
253
+ return projectMemberToApiMember(member);
254
+ }
255
+
256
+ @Get('me')
257
+ async me(@Req() req: Request): Promise<ApiMember> {
258
+ const member = await this.memberBase.resolveCurrentMember(req); // throws 401 if no cookie
259
+ return projectMemberToApiMember(member);
260
+ }
261
+
262
+ @Post('logout')
263
+ @HttpCode(204)
264
+ logout(@Res({ passthrough: true }) res: Response): void {
265
+ res.clearCookie('access_token');
266
+ res.clearCookie('refresh_token');
267
+ }
268
+ }
269
+ ```
270
+
271
+ The `projectMemberToApiMember` helper is host-specific — map your own
272
+ member shape to BPM's expectations, project Casbin roles into the BPM
273
+ role-literal strings (see "Mapping from a host RBAC system" below),
274
+ and compute `expiresAt` from the token TTL.
275
+
276
+ #### Pattern B — Run BPM on a separate auth host
277
+
278
+ When BPM lives on its own subdomain (e.g. `bpm.example.com`) separate
279
+ from the main app at `app.example.com`, you can run a thin wrapper
280
+ host that **only** owns the `/auth/*` endpoints and a session cookie
281
+ scoped to `*.example.com`. The BPM React client points at this
282
+ subdomain via `NEXT_PUBLIC_API_URL` / `NEXT_PUBLIC_API_AUTH_URL`
283
+ (or `configureBPMClient` for server-side). The wrapper host
284
+ internally calls back to the main app to verify the user.
285
+
286
+ This pattern is heavier (cross-origin cookies, CORS) but lets the main
287
+ app stay completely decoupled from BPM's auth surface. Use it only
288
+ when a same-origin deployment is impossible.
289
+
290
+ #### Cross-host auth precedence
291
+
292
+ If the consumer wants the React client to skip the BPM auth entirely
293
+ and rely on the host's existing session check at the Next.js layer,
294
+ the simplest path is to **not mount `<AuthProvider>` from
295
+ `@rytass/bpm-core-react`** and instead implement a host-side
296
+ `useAuth()` shim that returns the host's session. This is rare — most
297
+ consumers find Pattern A simpler.
298
+
201
299
  ## Role and Permission Contract
202
300
 
203
301
  BPM guards inspect `BPMAuthContext.roles` and `BPMAuthContext.permissions` with
@@ -215,6 +313,60 @@ clear admin / designer guards; otherwise BPM resolvers reject with
215
313
  `BPMAuthenticatedGuard`. The host does not need to add `@BPMAuthenticated()`
216
314
  on top of them.
217
315
 
316
+ ### Mapping from a host RBAC system (e.g. Casbin)
317
+
318
+ BPM does not call the host's RBAC engine — it reads exact-string `roles`
319
+ and `permissions` arrays off `BPMAuthContext`. Hosts using Casbin /
320
+ Permify / OPA must **project their grouping policy** into the BPM
321
+ literals inside their `authContextFactory`. A worked Casbin example:
322
+
323
+ ```ts
324
+ import { Inject, Injectable, type ExecutionContext } from '@nestjs/common';
325
+ import { GqlExecutionContext } from '@nestjs/graphql';
326
+ import { CASBIN_ENFORCER, type CasbinEnforcer } from '@your-host/auth';
327
+ import type { BPMAuthContext } from '@rytass/bpm-core-nestjs-module';
328
+
329
+ const HOST_TO_BPM_ROLES: Readonly<Record<string, string>> = {
330
+ approval_admin: 'BPM_ADMIN',
331
+ approval_designer: 'BPM_DESIGNER',
332
+ };
333
+
334
+ @Injectable()
335
+ export class BPMAuthContextFactory {
336
+ constructor(@Inject(CASBIN_ENFORCER) private readonly enforcer: CasbinEnforcer) {}
337
+
338
+ async build(ctx: ExecutionContext): Promise<BPMAuthContext | null> {
339
+ const req = GqlExecutionContext.create(ctx).getContext<{
340
+ req?: { member?: { id: string; email: string; name: string } };
341
+ }>().req;
342
+ const member = req?.member;
343
+ if (!member) return null;
344
+
345
+ // 1. Look up the host's Casbin role assignments for this member.
346
+ const hostRoles = await this.enforcer.getRolesForUser(member.id);
347
+
348
+ // 2. Translate into BPM's exact-string literals. Unknown host roles
349
+ // do nothing — BPM only checks for the documented strings.
350
+ const bpmRoles = hostRoles
351
+ .map((r) => HOST_TO_BPM_ROLES[r])
352
+ .filter((r): r is string => Boolean(r));
353
+
354
+ return {
355
+ memberId: member.id,
356
+ email: member.email,
357
+ name: member.name,
358
+ roles: bpmRoles,
359
+ permissions: [], // Host can also use `bpm:*` style permissions if preferred.
360
+ metadata: {},
361
+ };
362
+ }
363
+ }
364
+ ```
365
+
366
+ > The host owns the mapping table. Keep it in one place (a constant
367
+ > module or a database table) so adding a new BPM tier in a future
368
+ > BPMCore release becomes a one-line addition.
369
+
218
370
  ## Member Resolver
219
371
 
220
372
  The package resolves display names, email addresses, and approver candidates
@@ -334,74 +486,90 @@ and observable:
334
486
  carry an opaque `metadata` JSON object that BPM never introspects.
335
487
  Store your host's primary key here (e.g. `{ shuttleOrgUnitId: 12345 }`)
336
488
  so you can chase the back-pointer when reconciling.
337
- 3. **Soft delete via `deleteOrgUnit` / `deleteMembership`.** Deletes set
489
+ 3. **Soft delete via `deleteOrgUnit` / `deleteMembership` / `deleteManagerResolution`.** Deletes set
338
490
  `deletedAt` rather than removing rows. BPM's query layer hides
339
491
  soft-deleted rows automatically; reading `orgUnitCount()` will report
340
492
  the live count.
493
+ 4. **Positions are intentionally not deletable.** There is no
494
+ `deletePosition` mutation — positions are part of the historical
495
+ record (every signed approval task references the assignee's position
496
+ at sign time, so deleting a position would lose audit data). Retire a
497
+ position by removing all active memberships referencing it; the
498
+ position itself stays. Use `updatePosition({ id, name: 'retired-' + oldName, ... })`
499
+ if you want a UI hint that a position is no longer in use.
341
500
 
342
501
  ### Worked example: idempotent sync
343
502
 
344
503
  The example below uses only published exports from
345
- `@rytass/bpm-core-client/organization`. It pushes a host org tree into
346
- BPM, handling the create-or-update path by checking `code` matches.
347
- Verified exports: `orgUnits`, `createOrgUnit`, `updateOrgUnit`,
348
- `deleteOrgUnit`, `createPosition`, `createMembership`, `updateMembership`,
349
- `createManagerResolution`, `commitOrgUnitTreeDraft`.
504
+ `@rytass/bpm-core-client/organization`. It loads the entire BPM-side
505
+ state once via `readOrganizationDashboard()`, builds a `code → record`
506
+ index, then upserts each host node. Verified exports (all with flat
507
+ input shapes — no `{id, input: {...}}` wrapping):
508
+
509
+ - `readOrganizationDashboard({ orgUnitPageSize, orgUnitSearchText, ... })`
510
+ - `createOrgUnit({ code, name, type, parentId, metadataJson })`
511
+ - `updateOrgUnit({ id, code, name, type, parentId, metadataJson })`
512
+ - `deleteOrgUnit(id)` (soft-delete)
513
+ - `commitOrgUnitTreeDraft({ moves: { id, parentId, baseUpdatedAt }[] })` (transactional batch moves — each entry's `baseUpdatedAt` is the row's last-known `updatedAt`, used by BPM for optimistic-locking conflict detection)
514
+ - `createPosition` / `updatePosition` (key on `code`)
515
+ - `createMembership` / `updateMembership` (unique by `(memberId, orgUnitId, positionId)`)
516
+ - `createManagerResolution` / `updateManagerResolution`
350
517
 
351
518
  ```ts
352
519
  import {
520
+ readOrganizationDashboard,
353
521
  createOrgUnit,
354
522
  updateOrgUnit,
355
- createPosition,
356
- createMembership,
357
- orgUnits,
358
523
  type OrgUnitRecord,
359
524
  } from '@rytass/bpm-core-client/organization';
525
+ import type { OrgUnitType } from '@rytass/bpm-core-shared';
360
526
 
361
527
  interface HostOrgUnit {
362
- readonly hostId: string; // e.g. ERP primary key — your host-FK
363
- readonly code: string; // stable across sync runs
528
+ readonly hostId: string; // e.g. ERP primary key — your host-FK
529
+ readonly code: string; // stable across sync runs
364
530
  readonly name: string;
365
- readonly type: 'COMPANY' | 'DIVISION' | 'DEPARTMENT' | 'TEAM';
531
+ readonly type: OrgUnitType; // 'company' | 'division' | 'department' | 'team'
366
532
  readonly parentCode: string | null;
367
533
  }
368
534
 
369
- async function syncOrgUnit(host: HostOrgUnit): Promise<OrgUnitRecord> {
370
- // 1. Look up by host's stable code; BPM enforces uniqueness.
371
- const existing = (
372
- await orgUnits({ page: 1, pageSize: 1, searchText: host.code })
373
- ).find((u) => u.code === host.code);
374
-
375
- const parentId = host.parentCode
376
- ? (await orgUnits({ searchText: host.parentCode })).find(
377
- (u) => u.code === host.parentCode,
378
- )?.id ?? null
379
- : null;
380
-
381
- // 2. Stash the host-FK back into metadata so future reconciliations
382
- // can chase the pointer either direction.
383
- const metadataJson = JSON.stringify({ hostId: host.hostId });
384
-
385
- if (existing) {
386
- return updateOrgUnit({
387
- id: existing.id,
388
- input: {
389
- code: host.code,
390
- name: host.name,
391
- type: host.type,
392
- parentId,
393
- metadataJson,
394
- },
395
- });
535
+ async function syncOrgTree(
536
+ hostNodes: readonly HostOrgUnit[],
537
+ ): Promise<void> {
538
+ // 1. Single round trip: load every existing BPM org unit, index by code.
539
+ const dashboard = await readOrganizationDashboard({ orgUnitPageSize: null });
540
+ const byCode = new Map<string, OrgUnitRecord>(
541
+ dashboard.orgUnits.map((unit) => [unit.code, unit]),
542
+ );
543
+
544
+ // 2. Topo-sort by parent so a parent always exists before its child
545
+ // is upserted (createOrgUnit needs the parentId to be valid).
546
+ const ordered = topoSortByParent(hostNodes);
547
+
548
+ for (const host of ordered) {
549
+ const parentId =
550
+ host.parentCode != null ? (byCode.get(host.parentCode)?.id ?? null) : null;
551
+ const metadataJson = JSON.stringify({ hostId: host.hostId });
552
+ const existing = byCode.get(host.code);
553
+
554
+ const saved = existing
555
+ ? await updateOrgUnit({
556
+ id: existing.id,
557
+ code: host.code,
558
+ name: host.name,
559
+ type: host.type,
560
+ parentId,
561
+ metadataJson,
562
+ })
563
+ : await createOrgUnit({
564
+ code: host.code,
565
+ name: host.name,
566
+ type: host.type,
567
+ parentId,
568
+ metadataJson,
569
+ });
570
+
571
+ byCode.set(saved.code, saved);
396
572
  }
397
-
398
- return createOrgUnit({
399
- code: host.code,
400
- name: host.name,
401
- type: host.type,
402
- parentId,
403
- metadataJson,
404
- });
405
573
  }
406
574
  ```
407
575
 
@@ -411,6 +579,19 @@ The same pattern works for `Position` (key on `code`), `Membership`
411
579
  tree-shape changes (multi-node moves under a single parent),
412
580
  `commitOrgUnitTreeDraft` accepts the full draft in one transaction.
413
581
 
582
+ > **Atomicity caveat.** Only `commitOrgUnitTreeDraft` is transactional.
583
+ > Sequential `createMembership` / `createPosition` calls are independent
584
+ > mutations — if the 401st call in a batch of 500 fails, the prior 400
585
+ > stay committed. Wrap your sync loop in a "from-cursor" resume strategy
586
+ > if you need at-least-once semantics across crashes.
587
+
588
+ > **Programmatic base URL.** Server-side scripts that aren't running
589
+ > under Next.js (cron workers, one-off seeds) cannot rely on
590
+ > `NEXT_PUBLIC_API_URL` resolution — call `configureBPMClient({ baseUrl,
591
+ > fetch, headers })` from `@rytass/bpm-core-client` once at startup to
592
+ > point the transport at the right host and inject a session cookie or
593
+ > bearer token.
594
+
414
595
  ### What you do NOT mirror
415
596
 
416
597
  Member identity itself stays in your host's user table — BPM reaches it
@@ -577,6 +758,32 @@ Required environment variables for direct Vault loading:
577
758
  | `VAULT_PASSWORD` | Vault userpass password. |
578
759
  | `VAULT_PATH` | Vault KV path, defaults to develop. |
579
760
 
761
+ ### Sharing a Postgres cluster with the host
762
+
763
+ Hosts that already run their own Postgres (and their own Vault path,
764
+ e.g. `shuttle/develop`) have three options for where BPM's 22 tables
765
+ live. Pick the one that matches your operational model:
766
+
767
+ | Option | When to use | Trade-off |
768
+ |---|---|---|
769
+ | **Same database, same schema (`public`)** | Tiny prototypes, monorepo dev | Table-name collision risk — BPM owns `org_units`, `positions`, etc., common names. Audit your host's tables before choosing. |
770
+ | **Same database, separate schema** (recommended) | Most production hosts | Clean isolation; pg_dump per-schema; one connection pool. Set `DB_SCHEMA=bpm_core` in BPM's Vault path. Host stays on its own schema (typically `public`). |
771
+ | **Separate database** | Hard multi-tenant or compliance boundary | Two pools; cross-schema joins impossible (BPM doesn't need them); migration runners stay completely independent. |
772
+
773
+ For option B (separate schema), the host's own TypeORM `forRoot()` and
774
+ BPM's `buildTypeOrmModuleOptions` create **two distinct connections**
775
+ to the same Postgres cluster with different `schema:` settings — that
776
+ is the cleanest separation Nest's TypeORM module supports.
777
+
778
+ Vault paths can be split independently: keep `shuttle/develop` for the
779
+ host and create `bpm_core/develop` for BPM secrets. BPM's helper reads
780
+ its own path; the host's auth module reads its own. They do not
781
+ interfere.
782
+
783
+ > **Schema migrations** are per-schema. Run BPM's migrations against
784
+ > the schema named in `DB_SCHEMA`; the host's migrations stay on its
785
+ > own schema. There is no cross-schema migration coordination.
786
+
580
787
  ## Migrations
581
788
 
582
789
  This package ships TypeORM migrations under `src/lib/migrations`.
@@ -660,6 +867,43 @@ bootstrap, `attachmentRoutePrefix` must be supplied at module wiring time:
660
867
  - A process can only mount BPM under a single prefix. Multi-tenant hosts that
661
868
  want different prefixes per tenant should run separate processes.
662
869
 
870
+ ### Cross-origin authentication
871
+
872
+ Signed attachment URLs carry the auth **inside the URL** itself —
873
+ specifically a query-string signature derived from
874
+ `attachmentSignedUrlSecret` and `attachmentSignedUrlTtlSeconds`. The
875
+ browser does **not** send the session cookie to fetch the attachment;
876
+ BPM's controller verifies the signature instead.
877
+
878
+ This means:
879
+
880
+ - The attachment URL is safe to embed in `<img>` / `<a href>` /
881
+ `<iframe>` tags across origins; CORS is not in the auth path.
882
+ - Anyone who possesses the URL (until TTL expiry) can fetch the file.
883
+ Choose `attachmentSignedUrlTtlSeconds` short enough that a leaked URL
884
+ expires before exploitation (we suggest **600s** in production).
885
+ - Hosts that need true cross-tenant isolation should keep
886
+ `attachmentPublicBaseUrl` pointing at a CDN edge that enforces its own
887
+ authz on top — BPM's signature is freshness, not authorization.
888
+
889
+ ### Disabling the `/auth/test-members` endpoint in production
890
+
891
+ Hosts wired with the demo seed (typical for `pnpm demo:reset` /
892
+ `pnpm staging:reset`) expose `GET /auth/test-members` so the login form
893
+ shows a clickable list of fixture accounts. **Do not ship this in
894
+ production.** Two ways to keep it out:
895
+
896
+ 1. The endpoint is **owned by the wrapper host**, not by BPM —
897
+ `@rytass/bpm-core-nestjs-module` does not register it. Simply omit
898
+ the wrapper module / controller that serves
899
+ `/auth/test-members`. The reference implementation in BPMCore's
900
+ `apps/api` only exposes it when `NODE_ENV !== 'production'`; copy
901
+ that pattern in your own auth controller.
902
+ 2. The browser-side `listApiTestMembers()` client function tolerates a
903
+ `404` / `401` and returns an empty array, so removing the endpoint
904
+ degrades gracefully — the login form simply hides the demo-account
905
+ picker.
906
+
663
907
  ## Notifications and SLA
664
908
 
665
909
  BPM creates in-app notifications by default. Email and webhook delivery are
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/bpm-core-nestjs-module",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Embeddable NestJS BPM approval workflow module: workflow engine, approval templates, forms, organization/member contracts, attachments, signatures, notifications, delegation, and TypeORM migrations.",
5
5
  "keywords": [
6
6
  "bpm",
@@ -141,7 +141,7 @@
141
141
  "@nestjs/core": "^11.0.0",
142
142
  "@nestjs/graphql": "^13.4.0",
143
143
  "@nestjs/typeorm": "^11.0.1",
144
- "@rytass/bpm-core-shared": "^0.1.5",
144
+ "@rytass/bpm-core-shared": "^0.1.7",
145
145
  "@rytass/secret-adapter-vault-nestjs": "^0.4.5 || ^0.5.0",
146
146
  "express": "^4.0.0 || ^5.0.0",
147
147
  "graphql": "^16.0.0",
@@ -1,3 +1,4 @@
1
1
  export * from './identity-options';
2
2
  export * from './member-base.adapter';
3
+ export * from './member-not-found.exception';
3
4
  export * from './member-resolver.interface';
@@ -3,5 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./identity-options"), exports);
5
5
  tslib_1.__exportStar(require("./member-base.adapter"), exports);
6
+ tslib_1.__exportStar(require("./member-not-found.exception"), exports);
6
7
  tslib_1.__exportStar(require("./member-resolver.interface"), exports);
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/identity/index.ts"],"names":[],"mappings":";;;AAAA,6DAAmC;AACnC,gEAAsC;AACtC,sEAA4C"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/identity/index.ts"],"names":[],"mappings":";;;AAAA,6DAAmC;AACnC,gEAAsC;AACtC,uEAA6C;AAC7C,sEAA4C"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Thrown by `BPMMemberResolver.resolve(memberId)` when the host's user
3
+ * directory has no entry for the requested id. BPM surfaces the error
4
+ * as a GraphQL `errors[]` entry on whichever query triggered the
5
+ * lookup; the caller receives a structured response rather than a
6
+ * synthetic placeholder member.
7
+ *
8
+ * Note: `BPMMemberResolver.resolveMany(ids)` has the **opposite**
9
+ * contract — unknown ids are silently omitted from the returned `Map`.
10
+ * Use this exception only inside the single-id `resolve` path.
11
+ *
12
+ * Hosts may throw any `Error` subclass; this class is provided as a
13
+ * recommended default so error messages stay consistent in BPM's logs.
14
+ */
15
+ export declare class MemberNotFoundException extends Error {
16
+ readonly memberId: string;
17
+ constructor(memberId: string, message?: string);
18
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemberNotFoundException = void 0;
4
+ /**
5
+ * Thrown by `BPMMemberResolver.resolve(memberId)` when the host's user
6
+ * directory has no entry for the requested id. BPM surfaces the error
7
+ * as a GraphQL `errors[]` entry on whichever query triggered the
8
+ * lookup; the caller receives a structured response rather than a
9
+ * synthetic placeholder member.
10
+ *
11
+ * Note: `BPMMemberResolver.resolveMany(ids)` has the **opposite**
12
+ * contract — unknown ids are silently omitted from the returned `Map`.
13
+ * Use this exception only inside the single-id `resolve` path.
14
+ *
15
+ * Hosts may throw any `Error` subclass; this class is provided as a
16
+ * recommended default so error messages stay consistent in BPM's logs.
17
+ */
18
+ class MemberNotFoundException extends Error {
19
+ constructor(memberId, message) {
20
+ super(message ?? `BPM member not found: ${memberId}`);
21
+ this.name = 'MemberNotFoundException';
22
+ this.memberId = memberId;
23
+ }
24
+ }
25
+ exports.MemberNotFoundException = MemberNotFoundException;
26
+ //# sourceMappingURL=member-not-found.exception.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"member-not-found.exception.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/identity/member-not-found.exception.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;GAaG;AACH,MAAa,uBAAwB,SAAQ,KAAK;IAGhD,YAAY,QAAgB,EAAE,OAAgB;QAC5C,KAAK,CAAC,OAAO,IAAI,yBAAyB,QAAQ,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;QACtC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AARD,0DAQC"}