@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 +67 -0
- package/README.md +292 -48
- package/package.json +2 -2
- package/src/lib/identity/index.d.ts +1 -0
- package/src/lib/identity/index.js +1 -0
- package/src/lib/identity/index.js.map +1 -1
- package/src/lib/identity/member-not-found.exception.d.ts +18 -0
- package/src/lib/identity/member-not-found.exception.js +26 -0
- package/src/lib/identity/member-not-found.exception.js.map +1 -0
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.
|
|
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
|
|
346
|
-
|
|
347
|
-
Verified exports
|
|
348
|
-
|
|
349
|
-
|
|
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;
|
|
363
|
-
readonly code: string;
|
|
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: '
|
|
531
|
+
readonly type: OrgUnitType; // 'company' | 'division' | 'department' | 'team'
|
|
366
532
|
readonly parentCode: string | null;
|
|
367
533
|
}
|
|
368
534
|
|
|
369
|
-
async function
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -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"}
|