@rytass/bpm-core-nestjs-module 0.14.0 → 0.16.2
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 +100 -0
- package/README.md +19 -13
- package/package.json +5 -5
- package/src/lib/attachment/attachment-options.d.ts +23 -10
- package/src/lib/attachment/attachment-options.js +17 -31
- package/src/lib/attachment/attachment-options.js.map +1 -1
- package/src/lib/attachment/attachment-request-origin.d.ts +19 -0
- package/src/lib/attachment/attachment-request-origin.js +51 -0
- package/src/lib/attachment/attachment-request-origin.js.map +1 -0
- package/src/lib/attachment/attachment.module.d.ts +1 -2
- package/src/lib/attachment/attachment.module.js.map +1 -1
- package/src/lib/attachment/attachment.queries.d.ts +2 -2
- package/src/lib/attachment/attachment.queries.js +9 -4
- package/src/lib/attachment/attachment.queries.js.map +1 -1
- package/src/lib/attachment/attachment.service.d.ts +6 -1
- package/src/lib/attachment/attachment.service.js +12 -5
- package/src/lib/attachment/attachment.service.js.map +1 -1
- package/src/lib/bpm/bpm-root-options.d.ts +3 -3
- package/src/lib/bpm/bpm-root-options.js +7 -1
- package/src/lib/bpm/bpm-root-options.js.map +1 -1
- package/src/lib/bpm/bpm-root.module.d.ts +1 -2
- package/src/lib/bpm/bpm-root.module.js.map +1 -1
- package/src/lib/bpm-auth/bpm-auth.options.d.ts +1 -2
- package/src/lib/calendar/calendar.module.d.ts +1 -2
- package/src/lib/calendar/calendar.module.js.map +1 -1
- package/src/lib/form/form-member-value.validator.d.ts +16 -0
- package/src/lib/form/form-member-value.validator.js +37 -0
- package/src/lib/form/form-member-value.validator.js.map +1 -0
- package/src/lib/form/form-schema.validator.js +90 -1
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.module.d.ts +1 -2
- package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
- package/src/lib/identity/identity.module.d.ts +1 -2
- package/src/lib/identity/identity.module.js.map +1 -1
- package/src/lib/migrations/0000000026000-approval-instance-business-key.d.ts +20 -0
- package/src/lib/migrations/0000000026000-approval-instance-business-key.js +44 -0
- package/src/lib/migrations/0000000026000-approval-instance-business-key.js.map +1 -0
- package/src/lib/migrations/0000000027000-approval-instance-submitted-by.d.ts +19 -0
- package/src/lib/migrations/0000000027000-approval-instance-submitted-by.js +42 -0
- package/src/lib/migrations/0000000027000-approval-instance-submitted-by.js.map +1 -0
- package/src/lib/migrations/index.d.ts +2 -0
- package/src/lib/migrations/index.js +6 -0
- package/src/lib/migrations/index.js.map +1 -1
- package/src/lib/notification/notification-options.module.d.ts +1 -2
- package/src/lib/notification/notification-options.module.js.map +1 -1
- package/src/lib/notification/notification-template.js +1 -1
- package/src/lib/notification/notification-template.js.map +1 -1
- package/src/lib/notification/notification.service.d.ts +16 -0
- package/src/lib/notification/notification.service.js +54 -0
- package/src/lib/notification/notification.service.js.map +1 -1
- package/src/lib/organization/index.d.ts +1 -0
- package/src/lib/organization/index.js +1 -0
- package/src/lib/organization/index.js.map +1 -1
- package/src/lib/organization/organization-time-zone.d.ts +46 -0
- package/src/lib/organization/organization-time-zone.js +47 -0
- package/src/lib/organization/organization-time-zone.js.map +1 -0
- package/src/lib/organization/organization.service.d.ts +3 -1
- package/src/lib/organization/organization.service.js +8 -11
- package/src/lib/organization/organization.service.js.map +1 -1
- package/src/lib/signature/signature.module.d.ts +1 -2
- package/src/lib/signature/signature.module.js.map +1 -1
- package/src/lib/template/template.service.js +64 -0
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/workflow-engine/approval-instance.entity.d.ts +13 -0
- package/src/lib/workflow-engine/approval-instance.entity.js +20 -0
- package/src/lib/workflow-engine/approval-instance.entity.js.map +1 -1
- package/src/lib/workflow-engine/instance-submitter.d.ts +2 -0
- package/src/lib/workflow-engine/instance-submitter.js +14 -0
- package/src/lib/workflow-engine/instance-submitter.js.map +1 -0
- package/src/lib/workflow-engine/task.entity.d.ts +9 -0
- package/src/lib/workflow-engine/task.entity.js +13 -0
- package/src/lib/workflow-engine/task.entity.js.map +1 -1
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.js +18 -6
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.module.d.ts +1 -2
- package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.mutations.js +5 -0
- package/src/lib/workflow-engine/workflow-engine.mutations.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +83 -6
- package/src/lib/workflow-engine/workflow-engine.service.js +731 -230
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-options.module.d.ts +1 -2
- package/src/lib/workflow-webhook/workflow-webhook-options.module.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook.module.d.ts +1 -2
- package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
+
## 0.16.2 (2026-10-14)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- **react:** support react-pdf 11 and pdfjs-dist 6 in the PDF preview ([ab7f3ca](https://github.com/Rytass/BPMCore/commit/ab7f3ca))
|
|
6
|
+
|
|
7
|
+
### 🩹 Fixes
|
|
8
|
+
|
|
9
|
+
- **workflow-engine:** count only the member's own tasks on the dashboard ([8f12de3](https://github.com/Rytass/BPMCore/commit/8f12de3))
|
|
10
|
+
|
|
11
|
+
### ❤️ Thank You
|
|
12
|
+
|
|
13
|
+
- Chia Yu Pai @fantasywind
|
|
14
|
+
- Kai-Chieh Yang
|
|
15
|
+
|
|
16
|
+
## 0.16.0 (2026-10-13)
|
|
17
|
+
|
|
18
|
+
### 🚀 Features
|
|
19
|
+
|
|
20
|
+
- ⚠️ **attachment:** boot without attachment settings ([808bc65](https://github.com/Rytass/BPMCore/commit/808bc65))
|
|
21
|
+
|
|
22
|
+
### ⚠️ Breaking Changes
|
|
23
|
+
|
|
24
|
+
- **attachment:** boot without attachment settings ([808bc65](https://github.com/Rytass/BPMCore/commit/808bc65))
|
|
25
|
+
BPMResolvedAttachmentOptions.publicBaseUrl is
|
|
26
|
+
string | null, DEFAULT_BPM_ATTACHMENT_OPTIONS no longer carries a
|
|
27
|
+
signedUrlSecret, and attachmentAllowInsecureSignedUrlSecret is
|
|
28
|
+
deprecated and ignored. A host running more than one API replica must
|
|
29
|
+
set a shared attachmentSignedUrlSecret, since a generated one only
|
|
30
|
+
verifies on the process that signed the URL.
|
|
31
|
+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
32
|
+
|
|
33
|
+
### ❤️ Thank You
|
|
34
|
+
|
|
35
|
+
- Chia Yu Pai @fantasywind
|
|
36
|
+
- Claude Opus 5.5
|
|
37
|
+
|
|
38
|
+
## 0.15.0 (2026-10-13)
|
|
39
|
+
|
|
40
|
+
### ⚠️ Breaking Changes
|
|
41
|
+
|
|
42
|
+
- **workflow-engine / organization:** the engine now reads "today" in the organization time zone (`organizationTimeZone`, default `Asia/Taipei`) instead of UTC. For a membership or manager rule whose `effectiveFrom` / `effectiveTo` is today, approver resolution applies or retires it from local midnight — up to eight hours earlier than before in UTC+8. A host whose organization dates are kept in UTC sets `organizationTimeZone: 'UTC'` before upgrading. Running instances keep their tasks. ([d6e43f3](https://github.com/Rytass/BPMCore/commit/d6e43f3); this note was missing from the generated changelog because the footer sits on a `docs:` commit)
|
|
43
|
+
- **template:** publishing now refuses a `DYNAMIC_FORM` resolver whose member field can be left empty — the field must be `required`, not `hidden`, and have no `visibleWhen`. Drafts still save; versions already published are not re-validated. ([b3a3950](https://github.com/Rytass/BPMCore/commit/b3a3950))
|
|
44
|
+
|
|
45
|
+
### 🗄️ Upgrade
|
|
46
|
+
|
|
47
|
+
- New migrations add nullable columns and indexes only; no data is dropped. Hosts run with `migrationsRun: false`, so **run the migrations before deploying 0.15.0**.
|
|
48
|
+
|
|
49
|
+
### 🚀 Features
|
|
50
|
+
|
|
51
|
+
- **organization:** read effective dates in a configurable organizationTimeZone ([905982c](https://github.com/Rytass/BPMCore/commit/905982c))
|
|
52
|
+
- **forms:** add store-only hidden fields and link display for text fields ([ae38a92](https://github.com/Rytass/BPMCore/commit/ae38a92))
|
|
53
|
+
- **workflow-engine:** expose the node name on tasks as nodeLabel ([25b0705](https://github.com/Rytass/BPMCore/commit/25b0705))
|
|
54
|
+
- **workflow-engine:** tell the applicant who submitted the document on their behalf ([3a20985](https://github.com/Rytass/BPMCore/commit/3a20985))
|
|
55
|
+
- **workflow-engine:** let the member who submitted for the applicant read the instance ([7416df8](https://github.com/Rytass/BPMCore/commit/7416df8))
|
|
56
|
+
- **workflow-engine:** keep the member who submitted on someone's behalf out of excluding nodes ([0e02334](https://github.com/Rytass/BPMCore/commit/0e02334))
|
|
57
|
+
- **workflow-engine:** add a nullable submitted-by column to approval instances ([06e7c97](https://github.com/Rytass/BPMCore/commit/06e7c97))
|
|
58
|
+
- **workflow-engine:** require delegates and transfer targets on manager chains to match the grade they replace ([2cc0432](https://github.com/Rytass/BPMCore/commit/2cc0432))
|
|
59
|
+
- **workflow-engine:** let hosts submit and look up instances by business key ([daa7d9d](https://github.com/Rytass/BPMCore/commit/daa7d9d))
|
|
60
|
+
- **workflow-engine:** add business_key column with per-template unique index ([cb1b9bf](https://github.com/Rytass/BPMCore/commit/cb1b9bf))
|
|
61
|
+
- **designer:** pick approvers from a member field and show readable dry-run assignees ([bc9592e](https://github.com/Rytass/BPMCore/commit/bc9592e))
|
|
62
|
+
- **forms:** add a member field type ([0fca8d4](https://github.com/Rytass/BPMCore/commit/0fca8d4))
|
|
63
|
+
|
|
64
|
+
### 🩹 Fixes
|
|
65
|
+
|
|
66
|
+
- **template-designer:** name a member-field dry-run assignee by its field label ([465fcc3](https://github.com/Rytass/BPMCore/commit/465fcc3))
|
|
67
|
+
- **workflow-engine:** read task node labels from the entity's own table path ([16d6e8e](https://github.com/Rytass/BPMCore/commit/16d6e8e))
|
|
68
|
+
- **template-designer:** sample a dry-run member field with a placeholder, not the initiator ([b567e41](https://github.com/Rytass/BPMCore/commit/b567e41))
|
|
69
|
+
- **template:** refuse a DYNAMIC_FORM resolver on a member field that can be left empty ([b3a3950](https://github.com/Rytass/BPMCore/commit/b3a3950))
|
|
70
|
+
- **workflow-engine:** end a dry-run skip reason with a full stop before the stop reason ([8a855f4](https://github.com/Rytass/BPMCore/commit/8a855f4))
|
|
71
|
+
- **organization:** default organizationTimeZone to Asia/Taipei ([cc6b908](https://github.com/Rytass/BPMCore/commit/cc6b908))
|
|
72
|
+
- **workflow-engine:** consume the token of a NOTIFY node skipped by its entry condition ([#78](https://github.com/Rytass/BPMCore/issues/78))
|
|
73
|
+
- **forms:** keep the new-tab cue out of copied link text ([4fcd8e8](https://github.com/Rytass/BPMCore/commit/4fcd8e8))
|
|
74
|
+
- **forms:** harden hidden fields and link display after review ([9ce1f82](https://github.com/Rytass/BPMCore/commit/9ce1f82))
|
|
75
|
+
- **workflow-engine:** normalise the ad-hoc task nodeLabel like the list paths ([ded39e9](https://github.com/Rytass/BPMCore/commit/ded39e9))
|
|
76
|
+
- **workflow-engine:** bound the submitter name lookup inside the approval transaction ([dbf1d3b](https://github.com/Rytass/BPMCore/commit/dbf1d3b))
|
|
77
|
+
- **workflow-engine:** index the submitter column that the sent list now matches ([557f458](https://github.com/Rytass/BPMCore/commit/557f458))
|
|
78
|
+
- **workflow-engine:** leave out the refusals of seats the initiator exclusion removes ([959912e](https://github.com/Rytass/BPMCore/commit/959912e))
|
|
79
|
+
- **workflow-engine:** keep every refused delegation on record when the dedupe or a kept stored member hides its row ([aad1c8c](https://github.com/Rytass/BPMCore/commit/aad1c8c))
|
|
80
|
+
- **workflow-engine:** skip the submitter before choosing the initiator-manager replacement tier ([355373e](https://github.com/Rytass/BPMCore/commit/355373e))
|
|
81
|
+
- **workflow-engine:** harden business key lookup and count its length in characters ([844ce82](https://github.com/Rytass/BPMCore/commit/844ce82))
|
|
82
|
+
- **forms:** keep member ids out of case titles and check member references on publish ([2b3c9c0](https://github.com/Rytass/BPMCore/commit/2b3c9c0))
|
|
83
|
+
- **workflow-engine:** name the fallback problem in the dry-run manager message ([7f571f0](https://github.com/Rytass/BPMCore/commit/7f571f0))
|
|
84
|
+
- **workflow-engine:** translate missing org units and finish the dry-run edge cases ([b528aaa](https://github.com/Rytass/BPMCore/commit/b528aaa))
|
|
85
|
+
- **workflow-engine:** reject malformed member values on submit ([f13113e](https://github.com/Rytass/BPMCore/commit/f13113e))
|
|
86
|
+
- **workflow-engine:** say 通知對象 for notify recipients ([226dbda](https://github.com/Rytass/BPMCore/commit/226dbda))
|
|
87
|
+
- **workflow-engine:** name the node and the org in approver-resolution errors and keep the dry run going ([f45bfaf](https://github.com/Rytass/BPMCore/commit/f45bfaf))
|
|
88
|
+
|
|
89
|
+
### 🔥 Performance
|
|
90
|
+
|
|
91
|
+
- **workflow-engine:** batch the grade lookups of a manager-chain node's delegations ([99ae382](https://github.com/Rytass/BPMCore/commit/99ae382))
|
|
92
|
+
- **workflow-engine:** read task node labels in Postgres instead of loading whole snapshots ([65bbbe1](https://github.com/Rytass/BPMCore/commit/65bbbe1))
|
|
93
|
+
- **notification:** name the submitter at submit instead of inside the approval transaction ([da64763](https://github.com/Rytass/BPMCore/commit/da64763))
|
|
94
|
+
|
|
95
|
+
### ❤️ Thank You
|
|
96
|
+
|
|
97
|
+
- Chia Yu Pai @fantasywind
|
|
98
|
+
- Claude Opus 5.5
|
|
99
|
+
- Kai-Chieh Yang
|
|
100
|
+
|
|
1
101
|
## 0.14.0 (2026-10-12)
|
|
2
102
|
|
|
3
103
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -703,6 +703,15 @@ export interface BPMMemberResolver {
|
|
|
703
703
|
|
|
704
704
|
## Organization Data Ownership
|
|
705
705
|
|
|
706
|
+
> **Upgrading past 0.14.0 — which day is "today".** The workflow engine now
|
|
707
|
+
> reads membership and manager-rule effective dates in the
|
|
708
|
+
> `organizationTimeZone` root option, default `Asia/Taipei`; it used to read
|
|
709
|
+
> the UTC date. Between local midnight and 08:00 a rule effective from today
|
|
710
|
+
> now applies, and one ending today no longer does, up to eight hours earlier
|
|
711
|
+
> than before. If your organization dates are kept in UTC, set
|
|
712
|
+
> `organizationTimeZone: 'UTC'` before upgrading. See
|
|
713
|
+
> [Configuration Reference](#configuration-reference).
|
|
714
|
+
|
|
706
715
|
> **Read this before integrating BPM into a host that already has its own
|
|
707
716
|
> organization model.** The org-integration shape is **asymmetric** with
|
|
708
717
|
> member integration above — knowing which side owns what saves a day of
|
|
@@ -936,12 +945,13 @@ BPMRootModule.forRootAsync({
|
|
|
936
945
|
| `formDataSourceRegistryProvider` | Empty catalog | Single host provider for versioned Select / AutoComplete / Radio / Checkbox DataSources. |
|
|
937
946
|
| `attachmentRoutePrefix` | `/attachments` | Drives both the BPM signed URL path and the Nest controller mount path for `AttachmentController`. Must be set at module wiring time (see "Attachment Storage" below). |
|
|
938
947
|
| `attachmentStorageProviderId` | `local` | Value stored on attachment metadata for the active adapter. |
|
|
939
|
-
| `attachmentPublicBaseUrl` |
|
|
940
|
-
| `attachmentSignedUrlSecret` |
|
|
941
|
-
| `attachmentAllowInsecureSignedUrlSecret` |
|
|
948
|
+
| `attachmentPublicBaseUrl` | requesting origin | Public base URL for signed attachment URLs. Unset, each URL uses the origin of the GraphQL request that asked for it (`X-Forwarded-Proto` / `X-Forwarded-Host`, else the request's protocol and `Host`), so a host behind a proxy needs no setting. |
|
|
949
|
+
| `attachmentSignedUrlSecret` | random per process | HMAC secret for signed attachment download/preview tokens. Unset, each process generates one at boot and warns on stderr; a URL then only verifies on the process that signed it, so set a shared secret before running more than one API replica. |
|
|
950
|
+
| `attachmentAllowInsecureSignedUrlSecret` | — | Deprecated and ignored: no signing secret ships in the package any more. |
|
|
942
951
|
| `attachmentSignedUrlTtlSeconds` | `300` | Signed attachment URL lifetime in seconds. |
|
|
943
952
|
| `identityMemberMetadataCacheTtlMs` | `300000` | Member metadata cache TTL. |
|
|
944
953
|
| `identityRegisterResolvers` | `true` | Set to `false` when the host already publishes `member` / `members` / `memberCount` / `searchMembers` GraphQL queries. `IdentityService` stays available either way. |
|
|
954
|
+
| `organizationTimeZone` | `Asia/Taipei` | IANA zone in which BPM reads "today" when it compares against the date-only `effectiveFrom` / `effectiveTo` of memberships and manager resolutions (manager chains, direct and org-unit managers, position levels, the initiator snapshot, SLA escalation, the "active only" organization views). Set `'UTC'` if your organization dates are kept in UTC. An unknown zone makes BPM fail at startup. Independent of `notificationSlaBusinessCalendarTimeZone`. Before this option the workflow engine read UTC while `OrganizationService` read `Asia/Taipei`; both now follow it. |
|
|
945
955
|
| `notificationInAppEnabled` | `true` | Enables in-app notification records. |
|
|
946
956
|
| `notificationEmailEnabled` | `auto` | Enables email when SMTP settings are complete. |
|
|
947
957
|
| `notificationEmailSmtpHost` | `null` | SMTP host. |
|
|
@@ -1667,16 +1677,12 @@ npm publish --access public
|
|
|
1667
1677
|
|
|
1668
1678
|
## Production Notes
|
|
1669
1679
|
|
|
1670
|
-
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
will fail to boot after upgrading. Set the secret, or set
|
|
1677
|
-
`attachmentAllowInsecureSignedUrlSecret: true` to acknowledge that the
|
|
1678
|
-
signing key is public and boot anyway. Never set that flag on anything
|
|
1679
|
-
serving real attachments.
|
|
1680
|
+
- BPM boots with no attachment options at all. An unset
|
|
1681
|
+
`attachmentSignedUrlSecret` is generated per process at boot (with a stderr
|
|
1682
|
+
warning) — never a value published in this package — so set a shared secret
|
|
1683
|
+
once the API runs more than one replica, or when a restart must not
|
|
1684
|
+
invalidate the signed URLs handed out in the last few minutes.
|
|
1685
|
+
`attachmentAllowInsecureSignedUrlSecret` is deprecated and ignored.
|
|
1680
1686
|
- Use a production storage adapter instead of local filesystem storage.
|
|
1681
1687
|
- Keep old signature keys available for verification after key rotation.
|
|
1682
1688
|
- Keep in-process schedulers disabled in API replicas unless a dedicated worker
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rytass/bpm-core-nestjs-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.2",
|
|
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",
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
"@nestjs/core": ">=10.0.0",
|
|
163
163
|
"@nestjs/graphql": ">=12.0.0",
|
|
164
164
|
"@nestjs/typeorm": ">=10.0.0",
|
|
165
|
-
"@rytass/bpm-core-shared": "^0.
|
|
165
|
+
"@rytass/bpm-core-shared": "^0.16.2",
|
|
166
166
|
"@rytass/secret-adapter-vault-nestjs": ">=0.4.5",
|
|
167
167
|
"express": ">=4.0.0",
|
|
168
168
|
"graphql": ">=16.0.0",
|
|
@@ -178,13 +178,13 @@
|
|
|
178
178
|
}
|
|
179
179
|
},
|
|
180
180
|
"dependencies": {
|
|
181
|
-
"@rytass/storages": "^0.
|
|
182
|
-
"@rytass/storages-adapter-local": "^0.
|
|
181
|
+
"@rytass/storages": "^0.3.0",
|
|
182
|
+
"@rytass/storages-adapter-local": "^0.3.0",
|
|
183
183
|
"cel-js": "^0.8.2",
|
|
184
184
|
"class-transformer": "^0.5.1",
|
|
185
185
|
"class-validator": "^0.15.1",
|
|
186
186
|
"handlebars": "^4.7.10",
|
|
187
|
-
"nodemailer": "^
|
|
187
|
+
"nodemailer": "^10.1.0",
|
|
188
188
|
"tslib": "^2.8.1"
|
|
189
189
|
}
|
|
190
190
|
}
|
|
@@ -3,6 +3,11 @@ export interface BPMRootAttachmentOptions {
|
|
|
3
3
|
/**
|
|
4
4
|
* Public HTTP base URL used to build signed attachment download and preview
|
|
5
5
|
* URLs.
|
|
6
|
+
*
|
|
7
|
+
* Optional: when unset, each URL is built from the origin of the GraphQL
|
|
8
|
+
* request that asked for it (`X-Forwarded-Proto` / `X-Forwarded-Host`, else
|
|
9
|
+
* the request's own protocol and `Host`), so a host behind a proxy or on
|
|
10
|
+
* localhost gets a working link without configuring it.
|
|
6
11
|
*/
|
|
7
12
|
readonly attachmentPublicBaseUrl?: string | null;
|
|
8
13
|
/**
|
|
@@ -13,18 +18,19 @@ export interface BPMRootAttachmentOptions {
|
|
|
13
18
|
*/
|
|
14
19
|
readonly attachmentRoutePrefix?: string | null;
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* Without it, a production process that never set
|
|
20
|
-
* `attachmentSignedUrlSecret` refuses to start. Set this only for a
|
|
21
|
-
* throwaway environment that happens to run with `NODE_ENV=production` and
|
|
22
|
-
* serves no real attachments — the built-in value is published in this
|
|
23
|
-
* package, so anyone can forge a signed attachment URL against it.
|
|
21
|
+
* @deprecated No longer has any effect. An unset
|
|
22
|
+
* `attachmentSignedUrlSecret` now gets a random per-process secret instead
|
|
23
|
+
* of a built-in one, so there is nothing insecure left to acknowledge.
|
|
24
24
|
*/
|
|
25
25
|
readonly attachmentAllowInsecureSignedUrlSecret?: boolean;
|
|
26
26
|
/**
|
|
27
27
|
* HMAC secret used to sign attachment download and preview tokens.
|
|
28
|
+
*
|
|
29
|
+
* Optional: when unset, every process generates its own random secret at
|
|
30
|
+
* boot and warns on stderr. That keeps the URLs unforgeable, but a URL only
|
|
31
|
+
* verifies on the process that signed it — set a shared secret once the API
|
|
32
|
+
* runs more than one replica, or when a restart must not invalidate the
|
|
33
|
+
* links handed out in the last `attachmentSignedUrlTtlSeconds`.
|
|
28
34
|
*/
|
|
29
35
|
readonly attachmentSignedUrlSecret?: string | null;
|
|
30
36
|
/**
|
|
@@ -37,14 +43,21 @@ export interface BPMRootAttachmentOptions {
|
|
|
37
43
|
readonly attachmentStorageProviderId?: string | null;
|
|
38
44
|
}
|
|
39
45
|
export interface BPMResolvedAttachmentOptions {
|
|
40
|
-
|
|
46
|
+
/** `null` builds each URL from the requesting origin. */
|
|
47
|
+
readonly publicBaseUrl: string | null;
|
|
41
48
|
readonly routePrefix: string;
|
|
42
49
|
readonly signedUrlSecret: string;
|
|
43
50
|
readonly signedUrlTtlSeconds: number;
|
|
44
51
|
readonly storageProviderId: string;
|
|
45
52
|
}
|
|
46
53
|
export declare const BPM_ATTACHMENT_OPTIONS: InjectionToken<BPMResolvedAttachmentOptions>;
|
|
47
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Defaults for every attachment option that has a fixed one. The signing
|
|
56
|
+
* secret has none on purpose: a value published in this package would make
|
|
57
|
+
* every signed URL forgeable, so an unset secret is generated per process by
|
|
58
|
+
* {@link resolveBPMAttachmentOptions}.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DEFAULT_BPM_ATTACHMENT_OPTIONS: Omit<BPMResolvedAttachmentOptions, 'signedUrlSecret'>;
|
|
48
61
|
export declare function resolveBPMAttachmentOptions(options?: BPMRootAttachmentOptions): BPMResolvedAttachmentOptions;
|
|
49
62
|
/**
|
|
50
63
|
* Normalizes an attachment route prefix into the form Nest expects on a
|
|
@@ -3,47 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DEFAULT_BPM_ATTACHMENT_OPTIONS = exports.BPM_ATTACHMENT_OPTIONS = void 0;
|
|
4
4
|
exports.resolveBPMAttachmentOptions = resolveBPMAttachmentOptions;
|
|
5
5
|
exports.resolveAttachmentControllerPath = resolveAttachmentControllerPath;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
7
|
exports.BPM_ATTACHMENT_OPTIONS = Symbol('BPM_ATTACHMENT_OPTIONS');
|
|
8
|
+
/**
|
|
9
|
+
* Defaults for every attachment option that has a fixed one. The signing
|
|
10
|
+
* secret has none on purpose: a value published in this package would make
|
|
11
|
+
* every signed URL forgeable, so an unset secret is generated per process by
|
|
12
|
+
* {@link resolveBPMAttachmentOptions}.
|
|
13
|
+
*/
|
|
7
14
|
exports.DEFAULT_BPM_ATTACHMENT_OPTIONS = {
|
|
8
|
-
publicBaseUrl:
|
|
15
|
+
publicBaseUrl: null,
|
|
9
16
|
routePrefix: '/attachments',
|
|
10
|
-
signedUrlSecret: 'bpm-core-local-attachment-url-key-v1',
|
|
11
17
|
signedUrlTtlSeconds: 300,
|
|
12
18
|
storageProviderId: 'local',
|
|
13
19
|
};
|
|
14
20
|
function resolveBPMAttachmentOptions(options = {}) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
// the process refuses to start instead, and a host that genuinely wants the
|
|
22
|
-
// development value there has to say so.
|
|
23
|
-
if (usesBuiltInSecret &&
|
|
24
|
-
process.env.NODE_ENV === 'production' &&
|
|
25
|
-
options.attachmentAllowInsecureSignedUrlSecret !== true) {
|
|
26
|
-
throw new Error([
|
|
27
|
-
'[@rytass/bpm-core-nestjs-module] attachmentSignedUrlSecret is unset, so BPM would sign attachment URLs with the built-in local development value that ships in this package — anyone could forge one.',
|
|
28
|
-
'Set BPMRootModuleOptions.attachmentSignedUrlSecret to a strong secret.',
|
|
29
|
-
'If this service never serves attachments, BPM still mounts AttachmentModule, so set attachmentAllowInsecureSignedUrlSecret: true to acknowledge that the signing key is public and boot anyway.',
|
|
30
|
-
].join(' '));
|
|
31
|
-
}
|
|
32
|
-
// Outside production, still warn everywhere except an explicit local
|
|
33
|
-
// development run. Gating the warning on production alone meant a staging
|
|
34
|
-
// host that never set the variable signed attachment URLs with the published
|
|
35
|
-
// constant — and said nothing about it. One extra line in a developer's
|
|
36
|
-
// console is the cheaper trade.
|
|
37
|
-
if (usesBuiltInSecret &&
|
|
38
|
-
process.env.NODE_ENV !== 'development' &&
|
|
39
|
-
process.env.NODE_ENV !== 'test') {
|
|
21
|
+
const configuredSignedUrlSecret = normalizeText(options.attachmentSignedUrlSecret);
|
|
22
|
+
// No secret is not a reason to refuse to boot — the module is meant to start
|
|
23
|
+
// with nothing but a database. A random per-process key keeps the signed
|
|
24
|
+
// URLs unforgeable; the warning is for the one thing it cannot do, which is
|
|
25
|
+
// verify a URL signed by another process.
|
|
26
|
+
if (!configuredSignedUrlSecret && process.env.NODE_ENV !== 'test') {
|
|
40
27
|
// eslint-disable-next-line no-console
|
|
41
|
-
console.warn('[@rytass/bpm-core-nestjs-module] attachmentSignedUrlSecret is
|
|
28
|
+
console.warn('[@rytass/bpm-core-nestjs-module] attachmentSignedUrlSecret is unset, so this process signs attachment URLs with a random key generated at boot. The URLs stay unforgeable, but each one only verifies on the process that signed it: set BPMRootModuleOptions.attachmentSignedUrlSecret to a shared secret before running more than one API replica, or if a restart must not invalidate the links handed out in the last few minutes.');
|
|
42
29
|
}
|
|
43
30
|
return {
|
|
44
31
|
publicBaseUrl: normalizePublicBaseUrl(options.attachmentPublicBaseUrl),
|
|
45
32
|
routePrefix: normalizeRoutePrefix(options.attachmentRoutePrefix),
|
|
46
|
-
signedUrlSecret:
|
|
33
|
+
signedUrlSecret: configuredSignedUrlSecret ?? (0, node_crypto_1.randomBytes)(32).toString('hex'),
|
|
47
34
|
signedUrlTtlSeconds: normalizePositiveInteger(options.attachmentSignedUrlTtlSeconds, exports.DEFAULT_BPM_ATTACHMENT_OPTIONS.signedUrlTtlSeconds),
|
|
48
35
|
storageProviderId: normalizeText(options.attachmentStorageProviderId) ??
|
|
49
36
|
exports.DEFAULT_BPM_ATTACHMENT_OPTIONS.storageProviderId,
|
|
@@ -60,8 +47,7 @@ function resolveAttachmentControllerPath(value) {
|
|
|
60
47
|
return normalizedPrefix.replace(/^\//, '');
|
|
61
48
|
}
|
|
62
49
|
function normalizePublicBaseUrl(value) {
|
|
63
|
-
|
|
64
|
-
return normalizedValue.replace(/\/+$/, '');
|
|
50
|
+
return normalizeText(value)?.replace(/\/+$/, '') ?? null;
|
|
65
51
|
}
|
|
66
52
|
function normalizeRoutePrefix(value) {
|
|
67
53
|
const normalizedValue = normalizeText(value) ?? exports.DEFAULT_BPM_ATTACHMENT_OPTIONS.routePrefix;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-options.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/attachment/attachment-options.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"attachment-options.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/attachment/attachment-options.ts"],"names":[],"mappings":";;;AAgFA,kEA+BC;AAQD,0EAMC;AA5HD,6CAA0C;AA4D7B,QAAA,sBAAsB,GACjC,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAEnC;;;;;GAKG;AACU,QAAA,8BAA8B,GAGvC;IACF,aAAa,EAAE,IAAI;IACnB,WAAW,EAAE,cAAc;IAC3B,mBAAmB,EAAE,GAAG;IACxB,iBAAiB,EAAE,OAAO;CAC3B,CAAC;AAEF,SAAgB,2BAA2B,CACzC,UAAoC,EAAE;IAEtC,MAAM,yBAAyB,GAAG,aAAa,CAC7C,OAAO,CAAC,yBAAyB,CAClC,CAAC;IAEF,6EAA6E;IAC7E,yEAAyE;IACzE,4EAA4E;IAC5E,0CAA0C;IAC1C,IAAI,CAAC,yBAAyB,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAClE,sCAAsC;QACtC,OAAO,CAAC,IAAI,CACV,waAAwa,CACza,CAAC;IACJ,CAAC;IAED,OAAO;QACL,aAAa,EAAE,sBAAsB,CAAC,OAAO,CAAC,uBAAuB,CAAC;QACtE,WAAW,EAAE,oBAAoB,CAAC,OAAO,CAAC,qBAAqB,CAAC;QAChE,eAAe,EACb,yBAAyB,IAAI,IAAA,yBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC9D,mBAAmB,EAAE,wBAAwB,CAC3C,OAAO,CAAC,6BAA6B,EACrC,sCAA8B,CAAC,mBAAmB,CACnD;QACD,iBAAiB,EACf,aAAa,CAAC,OAAO,CAAC,2BAA2B,CAAC;YAClD,sCAA8B,CAAC,iBAAiB;KACnD,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAgB,+BAA+B,CAC7C,KAAgC;IAEhC,MAAM,gBAAgB,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAErD,OAAO,gBAAgB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,sBAAsB,CAC7B,KAAgC;IAEhC,OAAO,aAAa,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC;AAC3D,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAgC;IAC5D,MAAM,eAAe,GACnB,aAAa,CAAC,KAAK,CAAC,IAAI,sCAA8B,CAAC,WAAW,CAAC;IACrE,MAAM,gBAAgB,GAAG,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC;QACtD,CAAC,CAAC,eAAe;QACjB,CAAC,CAAC,IAAI,eAAe,EAAE,CAAC;IAE1B,OAAO,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,aAAa,CAAC,KAAgC;IACrD,MAAM,YAAY,GAAG,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAEzC,OAAO,YAAY,IAAI,IAAI,CAAC;AAC9B,CAAC;AAED,SAAS,wBAAwB,CAC/B,KAAyB,EACzB,QAAgB;IAEhB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACxE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface RequestLike {
|
|
2
|
+
readonly headers?: Readonly<Record<string, string | string[] | undefined>>;
|
|
3
|
+
readonly protocol?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Origin (`<protocol>://<host>`) the client reached this API on, used to build
|
|
7
|
+
* signed attachment URLs when the host configured no public base URL.
|
|
8
|
+
*
|
|
9
|
+
* Proxy headers win over the socket's own values, because behind an ingress
|
|
10
|
+
* the socket sees plain HTTP on an internal address. Only the first value of a
|
|
11
|
+
* comma-separated proxy header is read — the one the outermost proxy set — and
|
|
12
|
+
* a value that is not a bare host or not `http` / `https` is ignored, so a
|
|
13
|
+
* crafted header cannot inject a path or a scheme into the URL. A forged
|
|
14
|
+
* `Host` only changes the URL returned to the client that sent it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readAttachmentRequestOrigin(request: RequestLike | undefined): string | null;
|
|
17
|
+
/** Resolver parameter carrying {@link readAttachmentRequestOrigin}. */
|
|
18
|
+
export declare const BPMAttachmentRequestOrigin: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BPMAttachmentRequestOrigin = void 0;
|
|
4
|
+
exports.readAttachmentRequestOrigin = readAttachmentRequestOrigin;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const graphql_1 = require("@nestjs/graphql");
|
|
7
|
+
const HOST_PATTERN = /^[A-Za-z0-9.-]+(:\d{1,5})?$|^\[[0-9A-Fa-f:.]+\](:\d{1,5})?$/u;
|
|
8
|
+
/**
|
|
9
|
+
* Origin (`<protocol>://<host>`) the client reached this API on, used to build
|
|
10
|
+
* signed attachment URLs when the host configured no public base URL.
|
|
11
|
+
*
|
|
12
|
+
* Proxy headers win over the socket's own values, because behind an ingress
|
|
13
|
+
* the socket sees plain HTTP on an internal address. Only the first value of a
|
|
14
|
+
* comma-separated proxy header is read — the one the outermost proxy set — and
|
|
15
|
+
* a value that is not a bare host or not `http` / `https` is ignored, so a
|
|
16
|
+
* crafted header cannot inject a path or a scheme into the URL. A forged
|
|
17
|
+
* `Host` only changes the URL returned to the client that sent it.
|
|
18
|
+
*/
|
|
19
|
+
function readAttachmentRequestOrigin(request) {
|
|
20
|
+
if (!request) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const host = readFirstHeaderValue(request, 'x-forwarded-host') ??
|
|
24
|
+
readFirstHeaderValue(request, 'host');
|
|
25
|
+
const protocol = (readFirstHeaderValue(request, 'x-forwarded-proto') ??
|
|
26
|
+
request.protocol ??
|
|
27
|
+
'http').toLowerCase();
|
|
28
|
+
if (!host || !HOST_PATTERN.test(host)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (protocol !== 'http' && protocol !== 'https') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return `${protocol}://${host}`;
|
|
35
|
+
}
|
|
36
|
+
/** Resolver parameter carrying {@link readAttachmentRequestOrigin}. */
|
|
37
|
+
exports.BPMAttachmentRequestOrigin = (0, common_1.createParamDecorator)((_data, context) => readAttachmentRequestOrigin(readRequest(context)));
|
|
38
|
+
function readRequest(context) {
|
|
39
|
+
if (context.getType() === 'graphql') {
|
|
40
|
+
return graphql_1.GqlExecutionContext.create(context).getContext()?.req;
|
|
41
|
+
}
|
|
42
|
+
return context.switchToHttp().getRequest();
|
|
43
|
+
}
|
|
44
|
+
function readFirstHeaderValue(request, name) {
|
|
45
|
+
const value = request.headers?.[name];
|
|
46
|
+
const firstValue = (Array.isArray(value) ? value[0] : value)
|
|
47
|
+
?.split(',')[0]
|
|
48
|
+
?.trim();
|
|
49
|
+
return firstValue || null;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=attachment-request-origin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-request-origin.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/attachment/attachment-request-origin.ts"],"names":[],"mappings":";;;AA0BA,kEAyBC;AAnDD,2CAAwE;AACxE,6CAAsD;AAWtD,MAAM,YAAY,GAChB,8DAA8D,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,SAAgB,2BAA2B,CACzC,OAAgC;IAEhC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GACR,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,CAAC;QACjD,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,CACf,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,CAAC;QAClD,OAAO,CAAC,QAAQ;QAChB,MAAM,CACP,CAAC,WAAW,EAAE,CAAC;IAEhB,IAAI,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,GAAG,QAAQ,MAAM,IAAI,EAAE,CAAC;AACjC,CAAC;AAED,uEAAuE;AAC1D,QAAA,0BAA0B,GAAG,IAAA,6BAAoB,EAC5D,CAAC,KAAc,EAAE,OAAyB,EAAiB,EAAE,CAC3D,2BAA2B,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CACpD,CAAC;AAEF,SAAS,WAAW,CAAC,OAAyB;IAC5C,IAAI,OAAO,CAAC,OAAO,EAAU,KAAK,SAAS,EAAE,CAAC;QAC5C,OAAO,6BAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAElD,EAAE,GAAG,CAAC;IACX,CAAC;IAED,OAAO,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAA2B,CAAC;AACtE,CAAC;AAED,SAAS,oBAAoB,CAC3B,OAAoB,EACpB,IAAY;IAEZ,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAC1D,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACf,EAAE,IAAI,EAAE,CAAC;IAEX,OAAO,UAAU,IAAI,IAAI,CAAC;AAC5B,CAAC"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { DynamicModule, InjectionToken, Provider } from '@nestjs/common';
|
|
2
|
-
import { ModuleMetadata } from '@nestjs/common/interfaces';
|
|
1
|
+
import { DynamicModule, InjectionToken, ModuleMetadata, Provider } from '@nestjs/common';
|
|
3
2
|
import { AttachmentStorage } from './attachment-storage.token';
|
|
4
3
|
import { BPMRootAttachmentOptions } from './attachment-options';
|
|
5
4
|
export interface AttachmentModuleOptions extends BPMRootAttachmentOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment.module.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/attachment/attachment.module.ts"],"names":[],"mappings":";;;;;AAAA,
|
|
1
|
+
{"version":3,"file":"attachment.module.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/attachment/attachment.module.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,6 @@ export declare class AttachmentQueries {
|
|
|
4
4
|
private readonly attachmentService;
|
|
5
5
|
constructor(attachmentService: AttachmentService);
|
|
6
6
|
attachments(instanceId: string, taskId?: string | null, formFieldPath?: string | null, currentMemberId?: string): Promise<readonly AttachmentEntity[]>;
|
|
7
|
-
attachmentDownloadUrl(id: string, _requestedByMemberId: string | null, currentMemberId: string): Promise<string>;
|
|
8
|
-
attachmentPreviewUrl(id: string, _requestedByMemberId: string | null, currentMemberId: string): Promise<string>;
|
|
7
|
+
attachmentDownloadUrl(id: string, _requestedByMemberId: string | null, currentMemberId: string, requestOrigin: string | null): Promise<string>;
|
|
8
|
+
attachmentPreviewUrl(id: string, _requestedByMemberId: string | null, currentMemberId: string, requestOrigin: string | null): Promise<string>;
|
|
9
9
|
}
|
|
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const graphql_1 = require("@nestjs/graphql");
|
|
6
6
|
const bpm_auth_1 = require("../bpm-auth");
|
|
7
7
|
const attachment_entity_1 = require("./attachment.entity");
|
|
8
|
+
const attachment_request_origin_1 = require("./attachment-request-origin");
|
|
8
9
|
const attachment_service_1 = require("./attachment.service");
|
|
9
10
|
let AttachmentQueries = class AttachmentQueries {
|
|
10
11
|
constructor(attachmentService) {
|
|
@@ -18,17 +19,19 @@ let AttachmentQueries = class AttachmentQueries {
|
|
|
18
19
|
taskId: taskId ?? null,
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
|
-
async attachmentDownloadUrl(id, _requestedByMemberId, currentMemberId) {
|
|
22
|
+
async attachmentDownloadUrl(id, _requestedByMemberId, currentMemberId, requestOrigin) {
|
|
22
23
|
return this.attachmentService.createSignedUrl({
|
|
23
24
|
disposition: 'attachment',
|
|
24
25
|
id,
|
|
26
|
+
requestOrigin,
|
|
25
27
|
requestedByMemberId: currentMemberId,
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
|
-
async attachmentPreviewUrl(id, _requestedByMemberId, currentMemberId) {
|
|
30
|
+
async attachmentPreviewUrl(id, _requestedByMemberId, currentMemberId, requestOrigin) {
|
|
29
31
|
return this.attachmentService.createSignedUrl({
|
|
30
32
|
disposition: 'inline',
|
|
31
33
|
id,
|
|
34
|
+
requestOrigin,
|
|
32
35
|
requestedByMemberId: currentMemberId,
|
|
33
36
|
});
|
|
34
37
|
}
|
|
@@ -53,8 +56,9 @@ tslib_1.__decorate([
|
|
|
53
56
|
type: () => String,
|
|
54
57
|
})),
|
|
55
58
|
tslib_1.__param(2, (0, bpm_auth_1.BPMCurrentMemberId)()),
|
|
59
|
+
tslib_1.__param(3, (0, attachment_request_origin_1.BPMAttachmentRequestOrigin)()),
|
|
56
60
|
tslib_1.__metadata("design:type", Function),
|
|
57
|
-
tslib_1.__metadata("design:paramtypes", [String, Object, String]),
|
|
61
|
+
tslib_1.__metadata("design:paramtypes", [String, Object, String, Object]),
|
|
58
62
|
tslib_1.__metadata("design:returntype", Promise)
|
|
59
63
|
], AttachmentQueries.prototype, "attachmentDownloadUrl", null);
|
|
60
64
|
tslib_1.__decorate([
|
|
@@ -66,8 +70,9 @@ tslib_1.__decorate([
|
|
|
66
70
|
type: () => String,
|
|
67
71
|
})),
|
|
68
72
|
tslib_1.__param(2, (0, bpm_auth_1.BPMCurrentMemberId)()),
|
|
73
|
+
tslib_1.__param(3, (0, attachment_request_origin_1.BPMAttachmentRequestOrigin)()),
|
|
69
74
|
tslib_1.__metadata("design:type", Function),
|
|
70
|
-
tslib_1.__metadata("design:paramtypes", [String, Object, String]),
|
|
75
|
+
tslib_1.__metadata("design:paramtypes", [String, Object, String, Object]),
|
|
71
76
|
tslib_1.__metadata("design:returntype", Promise)
|
|
72
77
|
], AttachmentQueries.prototype, "attachmentPreviewUrl", null);
|
|
73
78
|
exports.AttachmentQueries = AttachmentQueries = tslib_1.__decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment.queries.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/attachment/attachment.queries.ts"],"names":[],"mappings":";;;;AAAA,6CAAwD;AACxD,0CAAmE;AACnE,2DAAuD;AACvD,6DAAyD;AAIlD,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B,YAA6B,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAG/D,AAAN,KAAK,CAAC,WAAW,CAC6B,UAAkB,EAE9D,MAAsB,EAEtB,aAA6B,EACP,eAAwB;QAE9C,OAAO,IAAI,CAAC,iBAAiB,CAAC,eAAe,CAAC;YAC5C,aAAa,EAAE,aAAa,IAAI,IAAI;YACpC,UAAU;YACV,mBAAmB,EAAE,eAAe;YACpC,MAAM,EAAE,MAAM,IAAI,IAAI;SACvB,CAAC,CAAC;IACL,CAAC;IAGK,AAAN,KAAK,CAAC,qBAAqB,CACW,EAAU,EAO9C,oBAAmC,EACb,eAAuB;
|
|
1
|
+
{"version":3,"file":"attachment.queries.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/attachment/attachment.queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -28,9 +28,14 @@ export declare class AttachmentService {
|
|
|
28
28
|
readonly formData: Readonly<Record<string, unknown>>;
|
|
29
29
|
readonly instanceId: string;
|
|
30
30
|
}): Promise<void>;
|
|
31
|
-
createSignedUrl({ disposition, id, requestedByMemberId, }: {
|
|
31
|
+
createSignedUrl({ disposition, id, requestOrigin, requestedByMemberId, }: {
|
|
32
32
|
readonly disposition: AttachmentDisposition;
|
|
33
33
|
readonly id: string;
|
|
34
|
+
/**
|
|
35
|
+
* Origin the request came in on; builds the URL when no public base URL
|
|
36
|
+
* is configured. Without either, the URL is root-relative.
|
|
37
|
+
*/
|
|
38
|
+
readonly requestOrigin?: string | null;
|
|
34
39
|
readonly requestedByMemberId: string;
|
|
35
40
|
}): Promise<string>;
|
|
36
41
|
readSignedAttachment({ disposition, id, token, }: {
|
|
@@ -7,6 +7,7 @@ const common_1 = require("@nestjs/common");
|
|
|
7
7
|
const typeorm_1 = require("typeorm");
|
|
8
8
|
const typeorm_2 = require("@nestjs/typeorm");
|
|
9
9
|
const approval_instance_entity_1 = require("../workflow-engine/approval-instance.entity");
|
|
10
|
+
const instance_submitter_1 = require("../workflow-engine/instance-submitter");
|
|
10
11
|
const task_candidate_entity_1 = require("../workflow-engine/task-candidate.entity");
|
|
11
12
|
const task_decision_entity_1 = require("../workflow-engine/task-decision.entity");
|
|
12
13
|
const task_entity_1 = require("../workflow-engine/task.entity");
|
|
@@ -15,7 +16,7 @@ const attachment_options_1 = require("./attachment-options");
|
|
|
15
16
|
const attachment_entity_1 = require("./attachment.entity");
|
|
16
17
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
17
18
|
let AttachmentService = class AttachmentService {
|
|
18
|
-
constructor(attachmentRepository, approvalInstanceRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, storage, attachmentOptions = attachment_options_1.
|
|
19
|
+
constructor(attachmentRepository, approvalInstanceRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, storage, attachmentOptions = (0, attachment_options_1.resolveBPMAttachmentOptions)()) {
|
|
19
20
|
this.attachmentRepository = attachmentRepository;
|
|
20
21
|
this.approvalInstanceRepository = approvalInstanceRepository;
|
|
21
22
|
this.taskRepository = taskRepository;
|
|
@@ -103,7 +104,7 @@ let AttachmentService = class AttachmentService {
|
|
|
103
104
|
await attachmentRepository.save(nextAttachments);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
async createSignedUrl({ disposition, id, requestedByMemberId, }) {
|
|
107
|
+
async createSignedUrl({ disposition, id, requestOrigin = null, requestedByMemberId, }) {
|
|
107
108
|
const attachment = await this.readAttachmentOrThrow(id);
|
|
108
109
|
await this.assertAttachmentReadableByMember(attachment, requestedByMemberId);
|
|
109
110
|
if (disposition === 'inline' && attachment.mimeType !== 'application/pdf') {
|
|
@@ -115,7 +116,8 @@ let AttachmentService = class AttachmentService {
|
|
|
115
116
|
this.attachmentOptions.signedUrlTtlSeconds,
|
|
116
117
|
id,
|
|
117
118
|
}, this.attachmentOptions.signedUrlSecret);
|
|
118
|
-
|
|
119
|
+
const baseUrl = this.attachmentOptions.publicBaseUrl ?? requestOrigin ?? '';
|
|
120
|
+
return `${baseUrl}${this.attachmentOptions.routePrefix}/${id}/download?token=${encodeURIComponent(token)}&disposition=${disposition}`;
|
|
119
121
|
}
|
|
120
122
|
async readSignedAttachment({ disposition, id, token, }) {
|
|
121
123
|
const payload = verifyAttachmentToken(token, this.attachmentOptions.signedUrlSecret);
|
|
@@ -148,7 +150,11 @@ let AttachmentService = class AttachmentService {
|
|
|
148
150
|
where: { id: attachment.instanceId },
|
|
149
151
|
})
|
|
150
152
|
: null;
|
|
151
|
-
|
|
153
|
+
// The member who filed the instance on the applicant's behalf opens the
|
|
154
|
+
// instance detail, so they must be able to open its files as well.
|
|
155
|
+
if (instance &&
|
|
156
|
+
(instance.initiatorMemberId === requestedByMemberId ||
|
|
157
|
+
(0, instance_submitter_1.readDistinctSubmitterMemberId)(instance) === requestedByMemberId)) {
|
|
152
158
|
return;
|
|
153
159
|
}
|
|
154
160
|
const task = attachment.taskId
|
|
@@ -184,7 +190,8 @@ let AttachmentService = class AttachmentService {
|
|
|
184
190
|
const candidates = await this.taskCandidateRepository.find({
|
|
185
191
|
where: { taskId: (0, typeorm_1.In)([...taskIds]) },
|
|
186
192
|
});
|
|
187
|
-
return candidates.some((candidate) => candidate.memberId === memberId ||
|
|
193
|
+
return candidates.some((candidate) => candidate.memberId === memberId ||
|
|
194
|
+
candidate.originalMemberId === memberId);
|
|
188
195
|
}
|
|
189
196
|
async isMemberRelatedToTaskDecisions(taskIds, memberId) {
|
|
190
197
|
const decisions = await this.taskDecisionRepository.find({
|