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

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 (3) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +144 -9
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,53 @@ 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.9 — 2026-05-28
13
+
14
+ ### Documentation
15
+
16
+ - **README "Machine-to-machine authentication" section** added under
17
+ Auth Context. Documents three patterns for server-side scripts (org
18
+ seeds, cron workers, integration tests) to authenticate against BPM:
19
+ (1) service member + login flow with cookie-jar fetch (recommended),
20
+ (2) service token header for hosts that issue long-lived JWTs,
21
+ (3) direct cookie injection for testing only.
22
+ - **README "Coexisting with a host's existing `<AuthProvider>`"** added
23
+ under Auth Context. Explains that BPM's React provider is a separate
24
+ context safe to nest under a host provider, with three rules of
25
+ thumb (scope to sub-tree, share cookie jar same-origin, don't
26
+ double-wrap `<AuthProvider>` directly).
27
+
28
+ ### Why a patch
29
+
30
+ Documentation only.
31
+
32
+ ## 0.1.8 — 2026-05-28
33
+
34
+ ### Documentation
35
+
36
+ - **Casbin example BPMAuthContext shape corrected.** The 0.1.6/0.1.7
37
+ example returned `{ memberId, email, name, roles, permissions,
38
+ metadata }` — the actual `BPMAuthContext` interface is
39
+ `{ memberId, roles, permissions, metadata }` (no `email`, no `name`).
40
+ Example now stashes display data inside `metadata`, with an explicit
41
+ callout that name/email reach resolvers through `BPMMemberResolver`
42
+ instead.
43
+ - **`createPosition` / `updatePosition` / `createMembership` /
44
+ `updateMembership` / `createManagerResolution` exact signatures
45
+ added** to the org Worked Example section. The 0.1.7 README only
46
+ listed function names; downstream developers were guessing input
47
+ shapes (e.g. mandatory `level` and `metadataJson` on positions).
48
+ - **`metadataJson` write-only clarification.** The "host-FK stash"
49
+ rule was misleading: `metadataJson` is accepted by every mutation
50
+ but the records returned by `readOrganizationDashboard` do NOT
51
+ surface a `metadata` field. Updated to "always reconcile by `code`;
52
+ treat `metadataJson` as audit/debugging breadcrumb, not a live FK
53
+ pointer."
54
+
55
+ ### Why a patch
56
+
57
+ Documentation only. No source change.
58
+
12
59
  ## 0.1.7 — 2026-05-28
13
60
 
14
61
  ### Documentation
package/README.md CHANGED
@@ -296,6 +296,125 @@ the simplest path is to **not mount `<AuthProvider>` from
296
296
  `useAuth()` shim that returns the host's session. This is rare — most
297
297
  consumers find Pattern A simpler.
298
298
 
299
+ ### Machine-to-machine authentication
300
+
301
+ Server-side scripts (org seeds, cron workers, integration tests) call
302
+ `configureBPMClient` to set the GraphQL base URL and optional default
303
+ headers, then must establish a session. Three patterns work:
304
+
305
+ #### Pattern 1 — Service member + login flow (recommended)
306
+
307
+ Create a dedicated BPM admin member (e.g. `bpm-sync@svc.example.com`),
308
+ store its credentials in Vault, and let the script call `loginApi`
309
+ during bootstrap. The login response sets an HTTP-only session cookie
310
+ that subsequent `requestGraphQl` calls automatically include.
311
+
312
+ ```ts
313
+ import { configureBPMClient, loginApi } from '@rytass/bpm-core-client';
314
+
315
+ async function bootstrap(): Promise<void> {
316
+ configureBPMClient({
317
+ baseUrl: process.env.BPM_API_URL ?? 'http://localhost:17603',
318
+ fetch: globalThis.fetch,
319
+ });
320
+ await loginApi({
321
+ identifier: process.env.BPM_SYNC_IDENTIFIER!,
322
+ password: process.env.BPM_SYNC_PASSWORD!,
323
+ });
324
+ // Subsequent calls to readOrganizationDashboard / createOrgUnit / etc.
325
+ // automatically forward the session cookie.
326
+ }
327
+ ```
328
+
329
+ Caveats: `globalThis.fetch` on Node 20+ does **not** maintain a cookie
330
+ jar across calls by default — set up one of:
331
+
332
+ - `undici`'s `Agent` with `cookies` enabled, then `setGlobalDispatcher(agent)`
333
+ - the `tough-cookie` + `node-fetch-cookies` pairing
334
+ - BPM's `configureBPMClient({ fetch: cookieAwareFetch })` injection
335
+
336
+ Without a cookie jar, the login succeeds but no subsequent call carries
337
+ the session. Most production deployments already use `undici` with
338
+ cookie support; verify before shipping.
339
+
340
+ #### Pattern 2 — Service token header (when supported by host)
341
+
342
+ If your wrapper host issues a long-lived service token (e.g. a JWT
343
+ signed with a known shared secret), pass it through
344
+ `configureBPMClient`'s `headers`:
345
+
346
+ ```ts
347
+ configureBPMClient({
348
+ baseUrl: process.env.BPM_API_URL!,
349
+ headers: { Authorization: `Bearer ${process.env.BPM_SERVICE_TOKEN!}` },
350
+ });
351
+ ```
352
+
353
+ This requires the wrapper host's auth middleware to honor `Authorization`
354
+ headers (member-base hosts can be configured to do so via
355
+ `authStrategy: 'jwt'` in addition to `cookieMode: true`).
356
+
357
+ #### Pattern 3 — Direct cookie injection (testing only)
358
+
359
+ For integration tests where you've already obtained a session cookie
360
+ out-of-band (e.g. from a Playwright fixture), pass it raw:
361
+
362
+ ```ts
363
+ configureBPMClient({
364
+ baseUrl: 'http://localhost:17603',
365
+ headers: { Cookie: 'access_token=<jwt>; refresh_token=<jwt>' },
366
+ });
367
+ ```
368
+
369
+ Do not use this in production scripts — cookies expire and the script
370
+ must handle refresh, which means you actually want Pattern 1.
371
+
372
+ ### Coexisting with a host's existing `<AuthProvider>`
373
+
374
+ Many host applications (Shuttle, custom admin panels) already mount
375
+ their own `<AuthProvider>` at the root of the Next.js tree. BPM's
376
+ `<AuthProvider>` (mounted indirectly via `<BPMNextProviders>`) is a
377
+ **separate context** that only tracks BPM session state. The two do
378
+ not conflict — they coexist as parallel React contexts:
379
+
380
+ ```tsx
381
+ // app/layout.tsx — host's root layout (untouched)
382
+ import { HostAuthProvider } from '@your-host/auth';
383
+
384
+ export default function RootLayout({ children }) {
385
+ return (
386
+ <html><body>
387
+ <HostAuthProvider>{children}</HostAuthProvider>
388
+ </body></html>
389
+ );
390
+ }
391
+
392
+ // app/operations/approval/layout.tsx — BPM sub-tree
393
+ 'use client';
394
+ import { BPMNextProviders } from '@rytass/bpm-core-react/next';
395
+
396
+ export default function ApprovalLayout({ children }) {
397
+ return (
398
+ <BPMNextProviders loginPath="/login">
399
+ {children}
400
+ </BPMNextProviders>
401
+ );
402
+ }
403
+ ```
404
+
405
+ Rules of thumb:
406
+
407
+ - **Scope `<BPMNextProviders>` to the BPM sub-tree** — never put it at
408
+ the root. Otherwise every host page goes through BPM's auth gate,
409
+ which redirects unauthenticated users to BPM's `/login`.
410
+ - **BPM auth and host auth share the cookie jar** when same-origin.
411
+ If `<BPMNextProviders loginPath="/login">` and the host's auth share
412
+ `/login`, the same login form can satisfy both — implement the host
413
+ login endpoint to also satisfy `/auth/login` from BPM's contract.
414
+ - **Do not import `<AuthProvider>` from `@rytass/bpm-core-react`
415
+ directly** if you've already mounted `<BPMNextProviders>` —
416
+ double-wrapping creates redundant `/auth/me` polls.
417
+
299
418
  ## Role and Permission Contract
300
419
 
301
420
  BPM guards inspect `BPMAuthContext.roles` and `BPMAuthContext.permissions` with
@@ -353,16 +472,23 @@ export class BPMAuthContextFactory {
353
472
 
354
473
  return {
355
474
  memberId: member.id,
356
- email: member.email,
357
- name: member.name,
358
475
  roles: bpmRoles,
359
476
  permissions: [], // Host can also use `bpm:*` style permissions if preferred.
360
- metadata: {},
477
+ // The BPMAuthContext shape does NOT include name/email — those are
478
+ // surfaced through BPMMemberResolver instead. If you want them
479
+ // available to downstream resolvers without an extra resolver call,
480
+ // stash them in `metadata` (BPM passes it through unchanged).
481
+ metadata: { name: member.name, email: member.email },
361
482
  };
362
483
  }
363
484
  }
364
485
  ```
365
486
 
487
+ > **`BPMAuthContext` shape**: `{ memberId, roles, permissions, metadata }`
488
+ > — exactly four fields. The host's `name` / `email` are not part of the
489
+ > contract; resolvers call `BPMMemberResolver.resolve(memberId)` when
490
+ > they need display data.
491
+
366
492
  > The host owns the mapping table. Keep it in one place (a constant
367
493
  > module or a database table) so adding a new BPM tier in a future
368
494
  > BPMCore release becomes a one-line addition.
@@ -482,10 +608,15 @@ and observable:
482
608
  uniqueness; use your host's stable identifier here (e.g. an LDAP DN
483
609
  slug or a internal ERP code). On a re-sync, look up existing entities
484
610
  by `code` before deciding INSERT vs UPDATE.
485
- 2. **`metadata` is the host-FK stash.** Both `OrgUnit` and `Membership`
486
- carry an opaque `metadata` JSON object that BPM never introspects.
487
- Store your host's primary key here (e.g. `{ shuttleOrgUnitId: 12345 }`)
488
- so you can chase the back-pointer when reconciling.
611
+ 2. **`metadataJson` is write-only.** The mutations
612
+ (`createOrgUnit`/`updateOrgUnit`/`createPosition`/`updatePosition`)
613
+ accept a `metadataJson` string that BPM stores verbatim. **However**,
614
+ `OrgUnitRecord` and `PositionRecord` returned by
615
+ `readOrganizationDashboard` do NOT include the metadata field —
616
+ it's intentionally hidden so the JSON blob doesn't bloat every
617
+ pagination payload. **For reconciliation, always key on `code`**
618
+ (which IS returned). Treat `metadataJson` as a debugging/audit
619
+ stash, not a live FK pointer.
489
620
  3. **Soft delete via `deleteOrgUnit` / `deleteMembership` / `deleteManagerResolution`.** Deletes set
490
621
  `deletedAt` rather than removing rows. BPM's query layer hides
491
622
  soft-deleted rows automatically; reading `orgUnitCount()` will report
@@ -511,7 +642,11 @@ input shapes — no `{id, input: {...}}` wrapping):
511
642
  - `updateOrgUnit({ id, code, name, type, parentId, metadataJson })`
512
643
  - `deleteOrgUnit(id)` (soft-delete)
513
644
  - `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`)
645
+ - `createPosition({ code, name, level, metadataJson })` `level` (1+) controls hierarchy weight (higher = more senior, used by manager-resolution priority sort); `metadataJson` is required (pass `'{}'` if unused)
646
+ - `updatePosition({ id, code, name, level, metadataJson })` — every field except `id` is `T | null`; pass `null` to leave a field unchanged
647
+ - `createMembership({ memberId, orgUnitId, positionId, isPrimary, effectiveFrom, effectiveTo })` — `positionId` and `effectiveTo` accept `null`; `effectiveFrom` is an ISO timestamp string. Uniqueness key is `(memberId, orgUnitId, positionId)`
648
+ - `updateMembership({ id, orgUnitId, positionId, isPrimary, effectiveFrom, effectiveTo })` — every field except `id` is `T | null`
649
+ - `createManagerResolution({ scopeType, scopeId, managerMemberId, priority, effectiveFrom, effectiveTo })` — `scopeType` is `'MEMBER' | 'ORG_UNIT' | 'POSITION'`; `priority` orders the resolver chain (lower number = checked first)
515
650
  - `createMembership` / `updateMembership` (unique by `(memberId, orgUnitId, positionId)`)
516
651
  - `createManagerResolution` / `updateManagerResolution`
517
652
 
@@ -528,7 +663,7 @@ interface HostOrgUnit {
528
663
  readonly hostId: string; // e.g. ERP primary key — your host-FK
529
664
  readonly code: string; // stable across sync runs
530
665
  readonly name: string;
531
- readonly type: OrgUnitType; // 'company' | 'division' | 'department' | 'team'
666
+ readonly type: OrgUnitType; // 'COMPANY' | 'DIVISION' | 'DEPARTMENT' | 'TEAM'
532
667
  readonly parentCode: string | null;
533
668
  }
534
669
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/bpm-core-nestjs-module",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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.7",
144
+ "@rytass/bpm-core-shared": "^0.1.9",
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",