@90soft/parse-server-kit 2.10.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +166 -0
- package/CLAUDE.md +44 -8
- package/dist/decorators/types/cloudTypes.d.ts +12 -0
- package/dist/decorators/types/cloudTypes.d.ts.map +1 -1
- package/dist/files/fileAccess.d.ts +224 -0
- package/dist/files/fileAccess.d.ts.map +1 -0
- package/dist/files/fileAccess.js +294 -0
- package/dist/files/fileAccess.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/rateLimit.d.ts +25 -6
- package/dist/middleware/rateLimit.d.ts.map +1 -1
- package/dist/middleware/rateLimit.js +59 -13
- package/dist/middleware/rateLimit.js.map +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,172 @@ All notable changes to `@90soft/parse-server-kit`.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [2.12.0]
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **File access rules — a file's PATH can now carry a permission.**
|
|
16
|
+
|
|
17
|
+
Parse has no per-file permission and cannot have one: the file router is
|
|
18
|
+
handed a name, and a name does not know which row references it. So a file
|
|
19
|
+
url is a bearer capability — anyone holding the string gets the bytes, with
|
|
20
|
+
no session, no ACL check and no expiry. Fine for published pictures; not fine
|
|
21
|
+
for a document, where a url that leaks once works forever, for anyone.
|
|
22
|
+
|
|
23
|
+
Parse does give one hook, `beforeFind` on `Parse.File`. This release owns it
|
|
24
|
+
and makes it usable:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {registerFileAccessRule, privateFileRule, attachPrivateFile} from '@90soft/parse-server-kit';
|
|
28
|
+
|
|
29
|
+
registerFileAccessRule(privateFileRule()); // once, in cloud code
|
|
30
|
+
await attachPrivateFile(row, 'document', upload); // stores under private/<Class>/<objectId>/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A file stored under `private/<ClassName>/<objectId>/` is then served only to
|
|
34
|
+
a caller who can read THAT row, loaded **as the caller** so Parse evaluates
|
|
35
|
+
the row's ACL and the class's CLP exactly as it would for a query. The
|
|
36
|
+
permission is not a copy of the row's ACL; it IS the row's ACL — nothing to
|
|
37
|
+
stamp, nothing to sync, no second source of truth.
|
|
38
|
+
|
|
39
|
+
Why not signed urls: a signed url is the same bearer token on a timer. It
|
|
40
|
+
leaks the same ways, forces a refresh dance and cache invalidation on every
|
|
41
|
+
client, and cannot express "this account was banned thirty seconds ago". The
|
|
42
|
+
rule re-checks every request.
|
|
43
|
+
|
|
44
|
+
**Nothing is registered until you add a rule**, so upgrading changes no
|
|
45
|
+
behaviour in a project that does not opt in, and a project that already
|
|
46
|
+
registers its own file trigger is not quietly overridden.
|
|
47
|
+
|
|
48
|
+
**Five traps this exists to absorb**, each of which cost real debugging:
|
|
49
|
+
|
|
50
|
+
1. **There is exactly ONE slot.** File triggers are keyed on the pseudo-class
|
|
51
|
+
`@File`, so a second `beforeFind(Parse.File)` anywhere REPLACES the first
|
|
52
|
+
— Parse logs "Only the last one will be used" and keeps serving. Two
|
|
53
|
+
features each registering a rule would silently disable one. Hence a
|
|
54
|
+
registry: the kit holds the slot, rules compose in registration order.
|
|
55
|
+
2. **Response headers come from `afterFind`.** `beforeFind`'s
|
|
56
|
+
`responseHeaders` are discarded. A rule's `headers` are applied in the
|
|
57
|
+
right place, including on a refusal, so a denial is not cached either.
|
|
58
|
+
3. **Only a header authenticates.** The file GET route reads the session from
|
|
59
|
+
`X-Parse-Session-Token` and nowhere else — no query parameter, no cookie.
|
|
60
|
+
A browser `<img src>` therefore cannot fetch a governed file; fetch it and
|
|
61
|
+
bind a blob. The upside: nobody can "simplify" this by putting a session
|
|
62
|
+
token in a url, because Parse will not read one there.
|
|
63
|
+
4. **A throw is global.** This runs on every file download in the app, so a
|
|
64
|
+
mistake does not break documents — it blanks every image at once. The
|
|
65
|
+
public path is one string comparison before any `await`, a rule whose
|
|
66
|
+
predicate throws is skipped rather than fatal, and the behaviour is
|
|
67
|
+
tested.
|
|
68
|
+
5. **Fail-open is the invisible failure.** "I cannot tell who owns this" must
|
|
69
|
+
mean refuse. `isPrivatePath` (is it governed?) and `privateOwnerOf` (who
|
|
70
|
+
owns it?) are deliberately separate functions, because a single predicate
|
|
71
|
+
returning `owner | null` conflates them: it answers `null` both for an
|
|
72
|
+
ordinary image and for `private/<Class>/<id>/extra/doc.jpg`, one directory
|
|
73
|
+
deeper than expected — and would serve that document to anybody, with the
|
|
74
|
+
symptom that it loads perfectly.
|
|
75
|
+
|
|
76
|
+
`Parse.File.setDirectory` / `.name()` / `.directory()` are now declared, and
|
|
77
|
+
`Parse.Cloud.beforeFind` / `afterFind` accept a class as well as a class name.
|
|
78
|
+
`@types/parse` is a hand-written stub that predates file directories (and is
|
|
79
|
+
empty from 6.x), so these are declared once here rather than cast at every
|
|
80
|
+
call site.
|
|
81
|
+
|
|
82
|
+
Requires Parse Server **>= 9.5.0** for file directories; verified against
|
|
83
|
+
9.10.4. 20 tests.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- Development now runs against `parse-server` **9.10.4** (was 9.10.1). The peer
|
|
88
|
+
range is unchanged at `>=9.0.0` — nothing here needs the newer server except
|
|
89
|
+
the file directories above, which are a 9.5.0 feature.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## [2.11.0]
|
|
94
|
+
|
|
95
|
+
### Security
|
|
96
|
+
|
|
97
|
+
- **`rateLimit` could be bypassed completely, by anyone, without an account.**
|
|
98
|
+
The bucket key was `req.headers['x-parse-session-token'] || req.ip`. That
|
|
99
|
+
header is supplied by the caller and is **not validated at this point in the
|
|
100
|
+
pipeline** — the entity-route middleware runs before Parse Server resolves the
|
|
101
|
+
session — so sending a fresh random token on every request minted a fresh
|
|
102
|
+
bucket each time.
|
|
103
|
+
|
|
104
|
+
Measured against a limit of `max: 3`: ten requests with ten different random
|
|
105
|
+
tokens all succeeded. The limiter was decorative.
|
|
106
|
+
|
|
107
|
+
The key is now the **client address** (`req.ip`, falling back to the socket
|
|
108
|
+
address). An optional `keyBy` is available for apps that can derive a
|
|
109
|
+
trustworthy identity themselves.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
@CloudFunction({ rateLimit: { windowMs: 60_000, max: 5 } }) // keyed on address
|
|
113
|
+
@CloudFunction({ rateLimit: { windowMs: 60_000, max: 5, keyBy: r => r.apiKeyId } })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Two things to know when you adopt this:**
|
|
117
|
+
|
|
118
|
+
1. **Configure Express `trust proxy` precisely** — a hop count, or the proxy's
|
|
119
|
+
address — if you run behind nginx or a load balancer. `req.ip` honours that
|
|
120
|
+
setting, so a permissive `true` lets a caller spoof the key with
|
|
121
|
+
`X-Forwarded-For`. This is the same bypass Parse Server fixed for its own
|
|
122
|
+
limiter in 9.10.1. Unset, Express ignores the header, which is safe.
|
|
123
|
+
2. **Limits were previously per session token and are now per address**, so
|
|
124
|
+
several users behind one NAT now share a bucket. Raise `max` where that
|
|
125
|
+
matters, or supply `keyBy`.
|
|
126
|
+
|
|
127
|
+
- **The bucket store is now bounded** (10,000 clients per function, oldest
|
|
128
|
+
evicted). A caller rotating addresses could previously grow the map without
|
|
129
|
+
limit, turning the rate limiter into a memory-exhaustion vector.
|
|
130
|
+
|
|
131
|
+
### Added
|
|
132
|
+
|
|
133
|
+
- `resetRateLimits()`, exported for tests.
|
|
134
|
+
- `test/rateLimit.test.ts` — eight tests, including the bypass above as a
|
|
135
|
+
regression. Rate limiting previously had **no test coverage at all**; the
|
|
136
|
+
regression test was verified to fail against the old implementation.
|
|
137
|
+
|
|
138
|
+
### Changed
|
|
139
|
+
|
|
140
|
+
- **`engines.node` is now `>=20.19.0`** (was `>=18.0.0`). Parse Server 9
|
|
141
|
+
requires it, so the old floor advertised a combination that cannot work.
|
|
142
|
+
- **Peer floors raised** to `parse >= 8.0.0` and `parse-server >= 9.0.0` (were
|
|
143
|
+
`>= 5.0.0` and `>= 8.3.0`). Development and tests now run against
|
|
144
|
+
`parse@8.6.2` / `parse-server@9.10.1`; previously the kit was tested against
|
|
145
|
+
Parse JS SDK **5** while every consuming app ran 8.x. That gap matters:
|
|
146
|
+
SDK 6 made `Parse.Object.set`/`setACL`/`unset` and `Role.setName` **throw**
|
|
147
|
+
instead of taking an error callback, so such a failure escapes the
|
|
148
|
+
`catchError` convention entirely.
|
|
149
|
+
|
|
150
|
+
The floors stay permissive deliberately — nothing in a consuming app breaks on
|
|
151
|
+
install. **Parse Server ≥ 9.10.1 is strongly recommended**: the 9.5→9.10 line
|
|
152
|
+
is almost entirely security fixes, including account takeover through
|
|
153
|
+
auth-adapter identity spoofing, protected-field bypasses via logical
|
|
154
|
+
operators, dot-notation, relation counts and LiveQuery, and several stored-XSS
|
|
155
|
+
routes through file upload.
|
|
156
|
+
|
|
157
|
+
### Notes
|
|
158
|
+
|
|
159
|
+
- Rate limiting remains **per process**: with N app instances the effective
|
|
160
|
+
limit is N × `max`. For cross-instance enforcement use Parse Server's own
|
|
161
|
+
`rateLimit` server option with `redisUrl`.
|
|
162
|
+
- A unique index with a **value** predicate (e.g. "one active row per pair")
|
|
163
|
+
needs `partialFilterExpression`, not `partialFilterNulls` — the latter only
|
|
164
|
+
emits `$exists: true`. Use the `options` escape hatch, which is passed
|
|
165
|
+
straight through to MongoDB:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
compoundIndexes: [{
|
|
169
|
+
fields: ['trainee', 'sport'],
|
|
170
|
+
unique: true,
|
|
171
|
+
options: { partialFilterExpression: { status: 'active' } },
|
|
172
|
+
}]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
11
177
|
## [2.10.1]
|
|
12
178
|
|
|
13
179
|
Twelve fixes found by a full read of the source, each verified against a running
|
package/CLAUDE.md
CHANGED
|
@@ -187,7 +187,7 @@ Static-method decorator. Registers the function and wraps it with role checking.
|
|
|
187
187
|
interface RouteConfig {
|
|
188
188
|
methods: HttpMethod[]; // 'GET'|'POST'|'PUT'|'DELETE'|'PATCH'
|
|
189
189
|
requiresAuth?: boolean;
|
|
190
|
-
rateLimit?: {windowMs: number; max: number};
|
|
190
|
+
rateLimit?: {windowMs: number; max: number; keyBy?: (req) => string}; // keyed on client ADDRESS
|
|
191
191
|
validation?: Parse.Cloud.Validator;
|
|
192
192
|
description?: string;
|
|
193
193
|
requireRoles?: string[];
|
|
@@ -454,11 +454,35 @@ parent's final ACL and **before** saving. For status-derived visibility set in a
|
|
|
454
454
|
| `removeResultMiddleware` | Unwraps Parse's `{result: …}` response |
|
|
455
455
|
| `conditionalJsonMiddleware` | `express.json({limit:'10mb', type:['text/plain']})`, skipping `{mountPath}/files` |
|
|
456
456
|
| `validateFunctionRoutes` | Legacy `/functions/*` validation |
|
|
457
|
-
| `checkRateLimit(req,res,name,cfg)` | In-process token bucket; returns `false` after sending 429 |
|
|
457
|
+
| `checkRateLimit(req,res,name,cfg)` | In-process token bucket keyed on client address; returns `false` after sending 429 |
|
|
458
458
|
|
|
459
459
|
**Master key** is read from the `X-Parse-Master-Key` header (preferred) or the
|
|
460
|
-
request body (deprecated, warns once).
|
|
461
|
-
|
|
460
|
+
request body (deprecated, warns once).
|
|
461
|
+
|
|
462
|
+
### Rate limiting — read before relying on it
|
|
463
|
+
|
|
464
|
+
The bucket key is the **client address** (`req.ip`, falling back to the socket
|
|
465
|
+
address). Pass `keyBy` only if the app can derive an identity it actually trusts.
|
|
466
|
+
|
|
467
|
+
> Before 2.11.0 the key was `x-parse-session-token || req.ip`. That header is
|
|
468
|
+
> caller-supplied and unvalidated here — this middleware runs **before** Parse
|
|
469
|
+
> resolves the session — so a rotating random token minted a fresh bucket per
|
|
470
|
+
> request and the limit did nothing. Ten requests passed a limit of three.
|
|
471
|
+
|
|
472
|
+
Three constraints:
|
|
473
|
+
|
|
474
|
+
1. **`trust proxy` must be exact** — a hop count or the proxy's address — when
|
|
475
|
+
behind nginx or a load balancer. `req.ip` honours it, so a permissive `true`
|
|
476
|
+
lets a caller spoof the key via `X-Forwarded-For` (the bypass Parse Server
|
|
477
|
+
fixed for its own limiter in 9.10.1). Unset is safe: Express ignores the
|
|
478
|
+
header.
|
|
479
|
+
2. **Per process.** With N instances the effective limit is N × `max`. For
|
|
480
|
+
cross-instance enforcement use Parse Server's `rateLimit` option with
|
|
481
|
+
`redisUrl`.
|
|
482
|
+
3. **Per address, not per user.** Users behind one NAT share a bucket.
|
|
483
|
+
|
|
484
|
+
The store is bounded at 10,000 clients per function, oldest evicted, so a caller
|
|
485
|
+
rotating addresses cannot grow it without limit.
|
|
462
486
|
|
|
463
487
|
---
|
|
464
488
|
|
|
@@ -488,7 +512,9 @@ interface CompoundIndex {
|
|
|
488
512
|
unique?: boolean;
|
|
489
513
|
name?: string;
|
|
490
514
|
sparse?: boolean;
|
|
491
|
-
partialFilterNulls?: boolean; // only index docs where all fields
|
|
515
|
+
partialFilterNulls?: boolean; // only index docs where all fields EXIST
|
|
516
|
+
// For a VALUE predicate (e.g. one active row per pair) use the options escape hatch:
|
|
517
|
+
// options: {partialFilterExpression: {status: 'active'}} -> passed straight to MongoDB
|
|
492
518
|
fieldTypes?: Record<string, 1|-1|'text'|'2dsphere'|'hashed'>;
|
|
493
519
|
options?: Record<string, unknown>; // e.g. default_language
|
|
494
520
|
}
|
|
@@ -688,11 +714,21 @@ output for `[Versioning]`.
|
|
|
688
714
|
|
|
689
715
|
| | Requirement |
|
|
690
716
|
|---|---|
|
|
691
|
-
| Node |
|
|
692
|
-
| parse-server | ≥
|
|
717
|
+
| Node | **≥ 20.19** |
|
|
718
|
+
| parse-server | ≥ 9.0 (optional peer); **≥ 9.10.1 strongly recommended** — see below. ≥ 9.10.0 is required on MongoDB 8.3+ (GeoPoint distance queries 500 otherwise) |
|
|
693
719
|
| MongoDB | ≥ 7.0.16 for parse-server 9; **replica set** for transactions |
|
|
694
|
-
| Parse SDK | ≥
|
|
720
|
+
| Parse SDK | ≥ 8; developed and tested on 8.6.2 |
|
|
695
721
|
| TypeScript | any, with `experimentalDecorators: true` |
|
|
696
722
|
|
|
723
|
+
**Why ≥ 9.10.1 rather than merely 9.x:** the 9.5→9.10 releases are almost
|
|
724
|
+
entirely security fixes — account takeover through auth-adapter identity
|
|
725
|
+
spoofing, protected-field bypasses via logical operators, dot-notation, relation
|
|
726
|
+
counts and LiveQuery, multiple rate-limit bypasses, and several stored-XSS routes
|
|
727
|
+
through file upload. An older 9.x is not "stable", it is unpatched.
|
|
728
|
+
|
|
729
|
+
**`@types/parse` is far behind the runtime** (3.0.x against an 8.x SDK), which is
|
|
730
|
+
why reaching newer Parse APIs sometimes needs an `as unknown as` cast. That is
|
|
731
|
+
the types package, not this kit.
|
|
732
|
+
|
|
697
733
|
**Transactions and `@ParseVersionField` are MongoDB-only.** On Postgres the
|
|
698
734
|
version field is declared but never enforced.
|
|
@@ -12,9 +12,21 @@ export interface SwaggerDocConfig {
|
|
|
12
12
|
export interface RouteConfig {
|
|
13
13
|
methods: HttpMethod[];
|
|
14
14
|
requiresAuth?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Per-function rate limit, keyed on the CLIENT ADDRESS.
|
|
17
|
+
*
|
|
18
|
+
* Enforced per process: with N app instances the effective limit is N x `max`.
|
|
19
|
+
* For cross-instance enforcement use Parse Server's own `rateLimit` server
|
|
20
|
+
* option with `redisUrl`.
|
|
21
|
+
*
|
|
22
|
+
* Requires Express `trust proxy` to be configured precisely (a hop count or
|
|
23
|
+
* the proxy's address) when running behind a reverse proxy. A permissive
|
|
24
|
+
* value lets a caller spoof the key with `X-Forwarded-For`.
|
|
25
|
+
*/
|
|
15
26
|
rateLimit?: {
|
|
16
27
|
windowMs: number;
|
|
17
28
|
max: number;
|
|
29
|
+
keyBy?: (req: any) => string;
|
|
18
30
|
};
|
|
19
31
|
validation?: Parse.Cloud.Validator;
|
|
20
32
|
description?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudTypes.d.ts","sourceRoot":"","sources":["../../../src/decorators/types/cloudTypes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"cloudTypes.d.ts","sourceRoot":"","sources":["../../../src/decorators/types/cloudTypes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,CAAA;KAAE,CAAC;IAC5E,UAAU,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,eAAe,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACtE,2EAA2E;IAC3E,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File access rules — turn a file's PATH into a permission.
|
|
3
|
+
*
|
|
4
|
+
* Parse has no per-file permission and cannot have one: the file router is
|
|
5
|
+
* handed a name, and a name does not know which row references it. So a file
|
|
6
|
+
* url is a bearer capability — anyone holding the string gets the bytes, with
|
|
7
|
+
* no session, no ACL check and no expiry. For published pictures that is
|
|
8
|
+
* correct and cheap. For a document (a certificate, an ID photo, an invoice) it
|
|
9
|
+
* is not: a url that leaks once works forever, for anyone, which is a
|
|
10
|
+
* REVOCATION problem that signed urls do not solve — a signed url is the same
|
|
11
|
+
* bearer token on a timer, and it cannot express "this account was banned
|
|
12
|
+
* thirty seconds ago".
|
|
13
|
+
*
|
|
14
|
+
* Parse does give us one hook: `beforeFind` on `Parse.File`, which runs on
|
|
15
|
+
* every `GET /files/...` and can throw to refuse. This module owns that hook
|
|
16
|
+
* and makes it usable, because using it directly has five traps:
|
|
17
|
+
*
|
|
18
|
+
* 1. **There is exactly ONE slot.** File triggers are keyed on the
|
|
19
|
+
* pseudo-class `@File`, not on a class, so a second
|
|
20
|
+
* `beforeFind(Parse.File)` anywhere REPLACES the first — Parse logs
|
|
21
|
+
* "Only the last one will be used" and carries on serving. Two features
|
|
22
|
+
* each registering their own rule would silently disable one of them.
|
|
23
|
+
* Hence a registry: this module holds the slot, rules compose.
|
|
24
|
+
* 2. **The headers come from `afterFind`.** `beforeFind`'s `responseHeaders`
|
|
25
|
+
* are discarded; only the `afterFind` request's are written to the
|
|
26
|
+
* response. A rule that marks a response uncacheable has to act there.
|
|
27
|
+
* 3. **Only a header authenticates.** The file GET route reads the session
|
|
28
|
+
* from `X-Parse-Session-Token` and nowhere else — no query parameter, no
|
|
29
|
+
* cookie. A browser `<img src>` therefore cannot fetch a governed file;
|
|
30
|
+
* the client must fetch it and bind a blob. The upside is that nobody can
|
|
31
|
+
* "simplify" this by putting a session token in a url.
|
|
32
|
+
* 4. **A throw is global.** This runs on every file download in the app, so
|
|
33
|
+
* a mistake does not break documents, it blanks every image at once.
|
|
34
|
+
* Everything on the public path here is a string comparison, before any
|
|
35
|
+
* `await`, and cannot throw.
|
|
36
|
+
* 5. **Fail-open is the invisible failure.** If "I cannot tell who owns
|
|
37
|
+
* this" is treated as "public", the document is served and the symptom is
|
|
38
|
+
* that it loads perfectly. So governed-but-unidentifiable must REFUSE.
|
|
39
|
+
*
|
|
40
|
+
* **Nothing is registered until a rule is added.** Upgrading the kit therefore
|
|
41
|
+
* changes no behaviour in a project that does not opt in, and a project that
|
|
42
|
+
* already registers its own file trigger is not quietly overridden.
|
|
43
|
+
*
|
|
44
|
+
* The usual setup, in cloud code:
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* import {registerFileAccessRule, privateFileRule} from '@90soft/parse-server-kit';
|
|
48
|
+
* registerFileAccessRule(privateFileRule());
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* [privateFileRule] is the general case: a file stored under
|
|
52
|
+
* `private/<ClassName>/<objectId>/` is served only to a caller who can read
|
|
53
|
+
* THAT row, judged by Parse's own ACL and CLP. The permission is not a copy of
|
|
54
|
+
* the row's ACL; it IS the row's ACL — nothing to stamp, nothing to sync.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* What a FILE trigger is handed, which is not what a class trigger gets: there
|
|
58
|
+
* is no `object` and no `query`, only the file.
|
|
59
|
+
*
|
|
60
|
+
* Declared HERE rather than taken from the ambient Parse types on purpose.
|
|
61
|
+
* `@types/parse` does not describe file triggers at all, so a consumer relying
|
|
62
|
+
* on it could not name the argument of [evaluateFileAccess] — a published
|
|
63
|
+
* library should not require its callers to declare types for its own API.
|
|
64
|
+
*/
|
|
65
|
+
export interface FileTriggerRequest {
|
|
66
|
+
file?: Parse.File;
|
|
67
|
+
fileSize?: number;
|
|
68
|
+
master?: boolean;
|
|
69
|
+
isReadOnly?: boolean;
|
|
70
|
+
user?: Parse.User;
|
|
71
|
+
ip?: string;
|
|
72
|
+
forceDownload?: boolean;
|
|
73
|
+
responseHeaders?: Record<string, string>;
|
|
74
|
+
}
|
|
75
|
+
/** The decision a rule is asked to make. */
|
|
76
|
+
export interface FileAccessContext {
|
|
77
|
+
/** The stored path, including any directory prefix. */
|
|
78
|
+
path: string;
|
|
79
|
+
/** The caller, when they sent a session token. */
|
|
80
|
+
user?: Parse.User;
|
|
81
|
+
/** True for a master-key request. */
|
|
82
|
+
master: boolean;
|
|
83
|
+
}
|
|
84
|
+
/** One rule, governing some subset of stored files. */
|
|
85
|
+
export interface FileAccessRule {
|
|
86
|
+
/**
|
|
87
|
+
* Identifies the rule in diagnostics, and makes replacing one deliberate:
|
|
88
|
+
* registering the same name twice warns rather than stacking silently.
|
|
89
|
+
*/
|
|
90
|
+
name: string;
|
|
91
|
+
/**
|
|
92
|
+
* Does this rule govern that path?
|
|
93
|
+
*
|
|
94
|
+
* **Must not throw and should not await.** It is consulted for every file
|
|
95
|
+
* download in the application, including every public image.
|
|
96
|
+
*/
|
|
97
|
+
governs(path: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Decide. Throw a `Parse.Error` to refuse.
|
|
100
|
+
*
|
|
101
|
+
* Returning normally allows the download. Only called when [governs] said
|
|
102
|
+
* yes and the request is not master-key.
|
|
103
|
+
*/
|
|
104
|
+
authorize(context: FileAccessContext): Promise<void> | void;
|
|
105
|
+
/**
|
|
106
|
+
* Headers to add to a governed response, e.g. `Cache-Control`.
|
|
107
|
+
*
|
|
108
|
+
* Applied whenever [governs] matches — including on a refusal, so that a
|
|
109
|
+
* denial is not cached either.
|
|
110
|
+
*/
|
|
111
|
+
headers?: Record<string, string>;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The gate. Public files leave before anything is awaited.
|
|
115
|
+
*
|
|
116
|
+
* Exported so a project can unit-test its rules without going through HTTP.
|
|
117
|
+
*/
|
|
118
|
+
export declare function evaluateFileAccess(request: FileTriggerRequest): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Add the governed response's headers.
|
|
121
|
+
*
|
|
122
|
+
* Separate from [evaluateFileAccess] because Parse reads response headers from
|
|
123
|
+
* the `afterFind` request and ignores `beforeFind`'s.
|
|
124
|
+
*/
|
|
125
|
+
export declare function applyFileAccessHeaders(request: FileTriggerRequest): void;
|
|
126
|
+
/**
|
|
127
|
+
* Register a rule, attaching the Parse hooks on the first call.
|
|
128
|
+
*
|
|
129
|
+
* Order matters: rules are consulted in registration order and the first match
|
|
130
|
+
* decides, so register the most specific first.
|
|
131
|
+
*/
|
|
132
|
+
export declare function registerFileAccessRule(rule: FileAccessRule): void;
|
|
133
|
+
/** Registered rules, in order. For diagnostics and tests. */
|
|
134
|
+
export declare function getFileAccessRules(): FileAccessRule[];
|
|
135
|
+
/**
|
|
136
|
+
* Drop every rule. For tests.
|
|
137
|
+
*
|
|
138
|
+
* The Parse hooks stay attached — they are harmless with no rules, since every
|
|
139
|
+
* path is then public — and re-attaching would hit Parse's duplicate warning.
|
|
140
|
+
*/
|
|
141
|
+
export declare function clearFileAccessRules(): void;
|
|
142
|
+
/**
|
|
143
|
+
* The directory prefix marking a file as governed by [privateFileRule].
|
|
144
|
+
*
|
|
145
|
+
* Parse's own `validateDirectory` reserves `metadata` and requires a directory
|
|
146
|
+
* to match `^[a-zA-Z0-9][a-zA-Z0-9_\-/]*$`, so a single lowercase word is safe.
|
|
147
|
+
* It is baked into stored file names, so changing it would orphan every file
|
|
148
|
+
* already written under the old one.
|
|
149
|
+
*/
|
|
150
|
+
export declare const PRIVATE_PREFIX = "private";
|
|
151
|
+
/** Who a private file's path says owns it. */
|
|
152
|
+
export interface PrivateFileOwner {
|
|
153
|
+
className: string;
|
|
154
|
+
objectId: string;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Is this path governed at all?
|
|
158
|
+
*
|
|
159
|
+
* Deliberately separate from [privateOwnerOf], and the separation is the
|
|
160
|
+
* safety property: "under the prefix" and "parses into an owner" are different
|
|
161
|
+
* questions, and a single predicate returning `owner | null` conflates them
|
|
162
|
+
* into a fail-OPEN. It would answer `null` both for an ordinary image and for
|
|
163
|
+
* `private/<Class>/<id>/extra/doc.jpg` — one directory deeper than expected —
|
|
164
|
+
* and so serve that document to anybody.
|
|
165
|
+
*/
|
|
166
|
+
export declare function isPrivatePath(path: string): boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Read the owner out of a governed path, or null if it is not well formed.
|
|
169
|
+
*
|
|
170
|
+
* `null` means "I cannot tell who owns this", which the rule treats as a
|
|
171
|
+
* refusal — never as permission.
|
|
172
|
+
*/
|
|
173
|
+
export declare function privateOwnerOf(path: string): PrivateFileOwner | null;
|
|
174
|
+
/**
|
|
175
|
+
* The directory a row's private files live in.
|
|
176
|
+
*
|
|
177
|
+
* Throws rather than quietly producing something Parse will reject: a class
|
|
178
|
+
* whose name cannot be a path segment (`_User`, since a directory must start
|
|
179
|
+
* alphanumeric) surfaces here, at the one call site, instead of as a confusing
|
|
180
|
+
* upload failure later.
|
|
181
|
+
*/
|
|
182
|
+
export declare function privateDirectoryFor(className: string, objectId: string): string;
|
|
183
|
+
/**
|
|
184
|
+
* Store a file so that the owning row's ACL governs it.
|
|
185
|
+
*
|
|
186
|
+
* The row must already be saved — its objectId is part of the path, which is
|
|
187
|
+
* what lets the rule find the owner with no index and no second source of
|
|
188
|
+
* truth. For a new record that means save, attach, save again: one extra write
|
|
189
|
+
* on a rare operation.
|
|
190
|
+
*
|
|
191
|
+
* **This should be the only way an application stores a private file.** The
|
|
192
|
+
* prefix is the entire boundary: a document saved without it is simply public,
|
|
193
|
+
* and nothing anywhere will complain.
|
|
194
|
+
*
|
|
195
|
+
* Private files deliberately get no derivatives — no converted copy, no
|
|
196
|
+
* thumbnail, and no blurhash, which is a small preview of the document and
|
|
197
|
+
* travels inside DTOs where the document itself does not.
|
|
198
|
+
*/
|
|
199
|
+
export declare function attachPrivateFile(row: Parse.Object, field: string, upload: {
|
|
200
|
+
base64: string;
|
|
201
|
+
name?: string;
|
|
202
|
+
}): Promise<void>;
|
|
203
|
+
/** Options for [privateFileRule]. */
|
|
204
|
+
export interface PrivateFileRuleOptions {
|
|
205
|
+
/**
|
|
206
|
+
* Headers for a private response. Defaults to `private, no-store`.
|
|
207
|
+
*
|
|
208
|
+
* One cached private response served to the next caller undoes the whole
|
|
209
|
+
* check, and the symptom is that the document loads — so this defaults to
|
|
210
|
+
* the strict value rather than being left to the caller.
|
|
211
|
+
*/
|
|
212
|
+
headers?: Record<string, string>;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Serve a private file only to a caller who can read its owning row.
|
|
216
|
+
*
|
|
217
|
+
* The caller is resolved from their session token, so Parse evaluates the
|
|
218
|
+
* row's ACL and the class's CLP exactly as it would for a query. With no
|
|
219
|
+
* session the row is loaded with no auth, which is right rather than a
|
|
220
|
+
* shortcut: if the owning row is publicly readable then its file is public
|
|
221
|
+
* too, because the row's ACL is the whole rule.
|
|
222
|
+
*/
|
|
223
|
+
export declare function privateFileRule(options?: PrivateFileRuleOptions): FileAccessRule;
|
|
224
|
+
//# sourceMappingURL=fileAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileAccess.d.ts","sourceRoot":"","sources":["../../src/files/fileAccess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC;IAClB,qCAAqC;IACrC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAE/B;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE5D;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAiCD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,kBAAkB,GAC1B,IAAI,CAON;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAsBjE;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,IAAI,cAAc,EAAE,CAErD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,YAAY,CAAC;AAExC,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAWpE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc/E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,KAAK,CAAC,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,qCAAqC;AACrC,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,cAAc,CA8BhB"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* File access rules — turn a file's PATH into a permission.
|
|
4
|
+
*
|
|
5
|
+
* Parse has no per-file permission and cannot have one: the file router is
|
|
6
|
+
* handed a name, and a name does not know which row references it. So a file
|
|
7
|
+
* url is a bearer capability — anyone holding the string gets the bytes, with
|
|
8
|
+
* no session, no ACL check and no expiry. For published pictures that is
|
|
9
|
+
* correct and cheap. For a document (a certificate, an ID photo, an invoice) it
|
|
10
|
+
* is not: a url that leaks once works forever, for anyone, which is a
|
|
11
|
+
* REVOCATION problem that signed urls do not solve — a signed url is the same
|
|
12
|
+
* bearer token on a timer, and it cannot express "this account was banned
|
|
13
|
+
* thirty seconds ago".
|
|
14
|
+
*
|
|
15
|
+
* Parse does give us one hook: `beforeFind` on `Parse.File`, which runs on
|
|
16
|
+
* every `GET /files/...` and can throw to refuse. This module owns that hook
|
|
17
|
+
* and makes it usable, because using it directly has five traps:
|
|
18
|
+
*
|
|
19
|
+
* 1. **There is exactly ONE slot.** File triggers are keyed on the
|
|
20
|
+
* pseudo-class `@File`, not on a class, so a second
|
|
21
|
+
* `beforeFind(Parse.File)` anywhere REPLACES the first — Parse logs
|
|
22
|
+
* "Only the last one will be used" and carries on serving. Two features
|
|
23
|
+
* each registering their own rule would silently disable one of them.
|
|
24
|
+
* Hence a registry: this module holds the slot, rules compose.
|
|
25
|
+
* 2. **The headers come from `afterFind`.** `beforeFind`'s `responseHeaders`
|
|
26
|
+
* are discarded; only the `afterFind` request's are written to the
|
|
27
|
+
* response. A rule that marks a response uncacheable has to act there.
|
|
28
|
+
* 3. **Only a header authenticates.** The file GET route reads the session
|
|
29
|
+
* from `X-Parse-Session-Token` and nowhere else — no query parameter, no
|
|
30
|
+
* cookie. A browser `<img src>` therefore cannot fetch a governed file;
|
|
31
|
+
* the client must fetch it and bind a blob. The upside is that nobody can
|
|
32
|
+
* "simplify" this by putting a session token in a url.
|
|
33
|
+
* 4. **A throw is global.** This runs on every file download in the app, so
|
|
34
|
+
* a mistake does not break documents, it blanks every image at once.
|
|
35
|
+
* Everything on the public path here is a string comparison, before any
|
|
36
|
+
* `await`, and cannot throw.
|
|
37
|
+
* 5. **Fail-open is the invisible failure.** If "I cannot tell who owns
|
|
38
|
+
* this" is treated as "public", the document is served and the symptom is
|
|
39
|
+
* that it loads perfectly. So governed-but-unidentifiable must REFUSE.
|
|
40
|
+
*
|
|
41
|
+
* **Nothing is registered until a rule is added.** Upgrading the kit therefore
|
|
42
|
+
* changes no behaviour in a project that does not opt in, and a project that
|
|
43
|
+
* already registers its own file trigger is not quietly overridden.
|
|
44
|
+
*
|
|
45
|
+
* The usual setup, in cloud code:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* import {registerFileAccessRule, privateFileRule} from '@90soft/parse-server-kit';
|
|
49
|
+
* registerFileAccessRule(privateFileRule());
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* [privateFileRule] is the general case: a file stored under
|
|
53
|
+
* `private/<ClassName>/<objectId>/` is served only to a caller who can read
|
|
54
|
+
* THAT row, judged by Parse's own ACL and CLP. The permission is not a copy of
|
|
55
|
+
* the row's ACL; it IS the row's ACL — nothing to stamp, nothing to sync.
|
|
56
|
+
*/
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.PRIVATE_PREFIX = void 0;
|
|
59
|
+
exports.evaluateFileAccess = evaluateFileAccess;
|
|
60
|
+
exports.applyFileAccessHeaders = applyFileAccessHeaders;
|
|
61
|
+
exports.registerFileAccessRule = registerFileAccessRule;
|
|
62
|
+
exports.getFileAccessRules = getFileAccessRules;
|
|
63
|
+
exports.clearFileAccessRules = clearFileAccessRules;
|
|
64
|
+
exports.isPrivatePath = isPrivatePath;
|
|
65
|
+
exports.privateOwnerOf = privateOwnerOf;
|
|
66
|
+
exports.privateDirectoryFor = privateDirectoryFor;
|
|
67
|
+
exports.attachPrivateFile = attachPrivateFile;
|
|
68
|
+
exports.privateFileRule = privateFileRule;
|
|
69
|
+
/** Ordered; the first rule that governs a path decides it. */
|
|
70
|
+
const rules = [];
|
|
71
|
+
/** The hook is attached once, lazily, on the first registration. */
|
|
72
|
+
let attached = false;
|
|
73
|
+
/**
|
|
74
|
+
* 404, never 403.
|
|
75
|
+
*
|
|
76
|
+
* A 403 confirms the file is there and merely forbidden, which for a document
|
|
77
|
+
* is itself the leak — it tells the caller that a particular row has one.
|
|
78
|
+
* "Not found" is the same answer an invented name would get.
|
|
79
|
+
*/
|
|
80
|
+
function notFound() {
|
|
81
|
+
return new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'File not found');
|
|
82
|
+
}
|
|
83
|
+
/** The first rule governing this path, or undefined when the file is public. */
|
|
84
|
+
function ruleFor(path) {
|
|
85
|
+
for (const rule of rules) {
|
|
86
|
+
// A rule that throws here would blank every image in the app, so one
|
|
87
|
+
// misbehaving rule must not take the public path down with it.
|
|
88
|
+
try {
|
|
89
|
+
if (rule.governs(path))
|
|
90
|
+
return rule;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The gate. Public files leave before anything is awaited.
|
|
100
|
+
*
|
|
101
|
+
* Exported so a project can unit-test its rules without going through HTTP.
|
|
102
|
+
*/
|
|
103
|
+
async function evaluateFileAccess(request) {
|
|
104
|
+
const path = request.file?.name() ?? '';
|
|
105
|
+
const rule = ruleFor(path);
|
|
106
|
+
if (!rule)
|
|
107
|
+
return;
|
|
108
|
+
// Cloud code reading deliberately with the master key. Note this is NOT how
|
|
109
|
+
// a worker that fetches a url reaches a file: a plain GET carries no key.
|
|
110
|
+
if (request.master)
|
|
111
|
+
return;
|
|
112
|
+
await rule.authorize({
|
|
113
|
+
path,
|
|
114
|
+
user: request.user,
|
|
115
|
+
master: false,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Add the governed response's headers.
|
|
120
|
+
*
|
|
121
|
+
* Separate from [evaluateFileAccess] because Parse reads response headers from
|
|
122
|
+
* the `afterFind` request and ignores `beforeFind`'s.
|
|
123
|
+
*/
|
|
124
|
+
function applyFileAccessHeaders(request) {
|
|
125
|
+
const rule = ruleFor(request.file?.name() ?? '');
|
|
126
|
+
if (!rule?.headers)
|
|
127
|
+
return;
|
|
128
|
+
// The incoming object already carries `X-Content-Type-Options: nosniff`, so
|
|
129
|
+
// extend it rather than replacing it.
|
|
130
|
+
request.responseHeaders = { ...(request.responseHeaders ?? {}), ...rule.headers };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Register a rule, attaching the Parse hooks on the first call.
|
|
134
|
+
*
|
|
135
|
+
* Order matters: rules are consulted in registration order and the first match
|
|
136
|
+
* decides, so register the most specific first.
|
|
137
|
+
*/
|
|
138
|
+
function registerFileAccessRule(rule) {
|
|
139
|
+
const existing = rules.findIndex(r => r.name === rule.name);
|
|
140
|
+
if (existing >= 0) {
|
|
141
|
+
console.warn(`[FileAccess] Replacing existing rule "${rule.name}". Two rules with one name is almost always an accident.`);
|
|
142
|
+
rules.splice(existing, 1, rule);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
rules.push(rule);
|
|
146
|
+
}
|
|
147
|
+
if (attached)
|
|
148
|
+
return;
|
|
149
|
+
attached = true;
|
|
150
|
+
/*
|
|
151
|
+
* `Parse.File` is passed as the class, which Parse maps to `@File`. This is
|
|
152
|
+
* the only pair of triggers in a kit project that is not declared on a model
|
|
153
|
+
* with `@BeforeFind` / `@AfterFind`, because there is no model to declare it
|
|
154
|
+
* on.
|
|
155
|
+
*/
|
|
156
|
+
Parse.Cloud.beforeFind(Parse.File, evaluateFileAccess);
|
|
157
|
+
Parse.Cloud.afterFind(Parse.File, applyFileAccessHeaders);
|
|
158
|
+
}
|
|
159
|
+
/** Registered rules, in order. For diagnostics and tests. */
|
|
160
|
+
function getFileAccessRules() {
|
|
161
|
+
return [...rules];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Drop every rule. For tests.
|
|
165
|
+
*
|
|
166
|
+
* The Parse hooks stay attached — they are harmless with no rules, since every
|
|
167
|
+
* path is then public — and re-attaching would hit Parse's duplicate warning.
|
|
168
|
+
*/
|
|
169
|
+
function clearFileAccessRules() {
|
|
170
|
+
rules.length = 0;
|
|
171
|
+
}
|
|
172
|
+
// ── The private-file rule ───────────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* The directory prefix marking a file as governed by [privateFileRule].
|
|
175
|
+
*
|
|
176
|
+
* Parse's own `validateDirectory` reserves `metadata` and requires a directory
|
|
177
|
+
* to match `^[a-zA-Z0-9][a-zA-Z0-9_\-/]*$`, so a single lowercase word is safe.
|
|
178
|
+
* It is baked into stored file names, so changing it would orphan every file
|
|
179
|
+
* already written under the old one.
|
|
180
|
+
*/
|
|
181
|
+
exports.PRIVATE_PREFIX = 'private';
|
|
182
|
+
/**
|
|
183
|
+
* Is this path governed at all?
|
|
184
|
+
*
|
|
185
|
+
* Deliberately separate from [privateOwnerOf], and the separation is the
|
|
186
|
+
* safety property: "under the prefix" and "parses into an owner" are different
|
|
187
|
+
* questions, and a single predicate returning `owner | null` conflates them
|
|
188
|
+
* into a fail-OPEN. It would answer `null` both for an ordinary image and for
|
|
189
|
+
* `private/<Class>/<id>/extra/doc.jpg` — one directory deeper than expected —
|
|
190
|
+
* and so serve that document to anybody.
|
|
191
|
+
*/
|
|
192
|
+
function isPrivatePath(path) {
|
|
193
|
+
return typeof path === 'string' && path.startsWith(`${exports.PRIVATE_PREFIX}/`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Read the owner out of a governed path, or null if it is not well formed.
|
|
197
|
+
*
|
|
198
|
+
* `null` means "I cannot tell who owns this", which the rule treats as a
|
|
199
|
+
* refusal — never as permission.
|
|
200
|
+
*/
|
|
201
|
+
function privateOwnerOf(path) {
|
|
202
|
+
if (!isPrivatePath(path))
|
|
203
|
+
return null;
|
|
204
|
+
// Exactly: private / ClassName / objectId / filename
|
|
205
|
+
const parts = path.split('/');
|
|
206
|
+
if (parts.length !== 4)
|
|
207
|
+
return null;
|
|
208
|
+
const [, className, objectId, filename] = parts;
|
|
209
|
+
if (!className || !objectId || !filename)
|
|
210
|
+
return null;
|
|
211
|
+
return { className, objectId };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The directory a row's private files live in.
|
|
215
|
+
*
|
|
216
|
+
* Throws rather than quietly producing something Parse will reject: a class
|
|
217
|
+
* whose name cannot be a path segment (`_User`, since a directory must start
|
|
218
|
+
* alphanumeric) surfaces here, at the one call site, instead of as a confusing
|
|
219
|
+
* upload failure later.
|
|
220
|
+
*/
|
|
221
|
+
function privateDirectoryFor(className, objectId) {
|
|
222
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_]*$/.test(className)) {
|
|
223
|
+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${className} cannot own private files: Parse requires a directory segment to start with a letter or digit`);
|
|
224
|
+
}
|
|
225
|
+
if (!/^[a-zA-Z0-9]+$/.test(objectId)) {
|
|
226
|
+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'A private file needs the owning row saved first, so that it has an objectId');
|
|
227
|
+
}
|
|
228
|
+
return `${exports.PRIVATE_PREFIX}/${className}/${objectId}`;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Store a file so that the owning row's ACL governs it.
|
|
232
|
+
*
|
|
233
|
+
* The row must already be saved — its objectId is part of the path, which is
|
|
234
|
+
* what lets the rule find the owner with no index and no second source of
|
|
235
|
+
* truth. For a new record that means save, attach, save again: one extra write
|
|
236
|
+
* on a rare operation.
|
|
237
|
+
*
|
|
238
|
+
* **This should be the only way an application stores a private file.** The
|
|
239
|
+
* prefix is the entire boundary: a document saved without it is simply public,
|
|
240
|
+
* and nothing anywhere will complain.
|
|
241
|
+
*
|
|
242
|
+
* Private files deliberately get no derivatives — no converted copy, no
|
|
243
|
+
* thumbnail, and no blurhash, which is a small preview of the document and
|
|
244
|
+
* travels inside DTOs where the document itself does not.
|
|
245
|
+
*/
|
|
246
|
+
async function attachPrivateFile(row, field, upload) {
|
|
247
|
+
if (!row.id) {
|
|
248
|
+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Save the row before attaching a private file — its objectId is part of the path');
|
|
249
|
+
}
|
|
250
|
+
const file = new Parse.File(encodeURIComponent(upload.name ?? 'document'), {
|
|
251
|
+
base64: upload.base64,
|
|
252
|
+
});
|
|
253
|
+
// Sent as `X-Parse-File-Directory`; honoured by Parse Server from 9.5.0.
|
|
254
|
+
file.setDirectory(privateDirectoryFor(row.className, row.id));
|
|
255
|
+
const saved = await file.save({ useMasterKey: true });
|
|
256
|
+
row.set(field, saved);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Serve a private file only to a caller who can read its owning row.
|
|
260
|
+
*
|
|
261
|
+
* The caller is resolved from their session token, so Parse evaluates the
|
|
262
|
+
* row's ACL and the class's CLP exactly as it would for a query. With no
|
|
263
|
+
* session the row is loaded with no auth, which is right rather than a
|
|
264
|
+
* shortcut: if the owning row is publicly readable then its file is public
|
|
265
|
+
* too, because the row's ACL is the whole rule.
|
|
266
|
+
*/
|
|
267
|
+
function privateFileRule(options = {}) {
|
|
268
|
+
const headers = options.headers ?? { 'Cache-Control': 'private, no-store' };
|
|
269
|
+
return {
|
|
270
|
+
name: 'private-prefix',
|
|
271
|
+
headers,
|
|
272
|
+
governs: isPrivatePath,
|
|
273
|
+
async authorize({ path, user }) {
|
|
274
|
+
/*
|
|
275
|
+
* Under the prefix but not a shape we understand: REFUSE.
|
|
276
|
+
*
|
|
277
|
+
* Treating it as public because no owner could be found is the one
|
|
278
|
+
* failure mode of this design that nobody would notice, because the
|
|
279
|
+
* document would load perfectly.
|
|
280
|
+
*/
|
|
281
|
+
const owner = privateOwnerOf(path);
|
|
282
|
+
if (!owner)
|
|
283
|
+
throw notFound();
|
|
284
|
+
const sessionToken = user?.getSessionToken();
|
|
285
|
+
const query = new Parse.Query(owner.className);
|
|
286
|
+
const row = await (sessionToken
|
|
287
|
+
? query.get(owner.objectId, { sessionToken })
|
|
288
|
+
: query.get(owner.objectId)).catch(() => null);
|
|
289
|
+
if (!row)
|
|
290
|
+
throw notFound();
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=fileAccess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileAccess.js","sourceRoot":"","sources":["../../src/files/fileAccess.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { configureRoleCache, invalidateRoles, roleCacheEnabled, roleCacheStats }
|
|
|
24
24
|
export type { RoleCacheOptions } from './utils/roleCache';
|
|
25
25
|
export { validateFunctionRoutes, validateEntityRoutes, restrictRoutes, removeResultMiddleware, conditionalJsonMiddleware } from './middleware/middleware';
|
|
26
26
|
export { checkRateLimit } from './middleware/rateLimit';
|
|
27
|
+
export { registerFileAccessRule, getFileAccessRules, clearFileAccessRules, evaluateFileAccess, applyFileAccessHeaders, privateFileRule, attachPrivateFile, isPrivatePath, privateOwnerOf, privateDirectoryFor, PRIVATE_PREFIX } from './files/fileAccess';
|
|
28
|
+
export type { FileAccessRule, FileAccessContext, FileTriggerRequest, PrivateFileOwner, PrivateFileRuleOptions } from './files/fileAccess';
|
|
27
29
|
export { createSchemaConfig } from './database/schema';
|
|
28
30
|
export type { SchemaConfigOptions } from './database/schema';
|
|
29
31
|
export { applyAllIndexes, applyUniqueIndexes, getUniqueIndexes, getCompoundIndexes, getFieldIndexes } from './database/indexes';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjI,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAE3G,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC1G,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAC9E,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,0BAA0B,EAChE,cAAc,EAAE,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAChE,cAAc,EAAE,aAAa,EAC7B,gBAAgB,EAAE,eAAe,EACjC,aAAa,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE9E,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,qBAAqB,EAAE,gBAAgB,EACnF,sBAAsB,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAEpJ,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtH,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACnG,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAChF,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAG9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAG1H,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACxE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAC3D,sBAAsB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACrG,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGpD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC1G,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,cAAc,EACnE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// @90soft/parse-server-kit — complete toolkit for Parse Server
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.getUserRoles = exports.catchError = exports.MAX_QUERY_LIMIT = exports.roleKey = exports.UserRoles = exports.cloneAcl = exports.syncImageAcl = exports.implementACL = exports.BaseModel = exports.applyMongoValidators = exports.getValidators = exports.validateOrThrow = exports.validateObject = exports.classNames = exports.RouteRegistry = exports.Route = exports.CronRegistry = exports.CronSchedule = exports.Cron = exports.TriggerRegistry = exports.AfterEvent = exports.BeforeSubscribe = exports.BeforeConnect = exports.AfterSaveConfig = exports.BeforeSaveConfig = exports.AfterFindFile = exports.BeforeFindFile = exports.AfterDeleteFile = exports.BeforeDeleteFile = exports.AfterSaveFile = exports.BeforeSaveFile = exports.BeforePasswordResetRequest = exports.AfterLogout = exports.AfterLogin = exports.BeforeLogin = exports.AfterFind = exports.BeforeFind = exports.AfterDelete = exports.BeforeDelete = exports.AfterSave = exports.BeforeSave = exports.CloudFunctionRegistry = exports.onFunctionRegistered = exports.ProtectedCloudFunction = exports.CloudFunction = exports.onFieldRegistered = exports.onClassRegistered = exports.getSchemaDefinition = exports.ParseField = exports.ParseClass = void 0;
|
|
5
|
-
exports.
|
|
5
|
+
exports.generateSwaggerSpec = exports.SwaggerRegistry = exports.NO_AMBIENT_TRANSACTION = exports.withAmbientSession = exports.CONFLICT_MESSAGE = exports.CONFLICT = exports.useTransactionAdapter = exports.currentSession = exports.inTransaction = exports.withTransaction = exports.Transactional = exports.VERSION_CONFLICT_MESSAGE = exports.VERSION_CONFLICT = exports.createVersionedMongoAdapter = exports.VersionRegistry = exports.ParseVersionField = exports.getFieldIndexes = exports.getCompoundIndexes = exports.getUniqueIndexes = exports.applyUniqueIndexes = exports.applyAllIndexes = exports.createSchemaConfig = exports.PRIVATE_PREFIX = exports.privateDirectoryFor = exports.privateOwnerOf = exports.isPrivatePath = exports.attachPrivateFile = exports.privateFileRule = exports.applyFileAccessHeaders = exports.evaluateFileAccess = exports.clearFileAccessRules = exports.getFileAccessRules = exports.registerFileAccessRule = exports.checkRateLimit = exports.conditionalJsonMiddleware = exports.removeResultMiddleware = exports.restrictRoutes = exports.validateEntityRoutes = exports.validateFunctionRoutes = exports.roleCacheStats = exports.roleCacheEnabled = exports.invalidateRoles = exports.configureRoleCache = exports.importFiles = exports.sleep = exports.generateRandomString = exports.generateRandomInteger = exports.generateRandomPassword = exports.formatCount = exports.getUsersRoles = void 0;
|
|
6
|
+
exports.setupSwagger = exports.getSwaggerJson = void 0;
|
|
6
7
|
// ── Decorators ──
|
|
7
8
|
var parseDecorators_1 = require("./decorators/parseDecorators");
|
|
8
9
|
Object.defineProperty(exports, "ParseClass", { enumerable: true, get: function () { return parseDecorators_1.ParseClass; } });
|
|
@@ -96,6 +97,23 @@ Object.defineProperty(exports, "removeResultMiddleware", { enumerable: true, get
|
|
|
96
97
|
Object.defineProperty(exports, "conditionalJsonMiddleware", { enumerable: true, get: function () { return middleware_1.conditionalJsonMiddleware; } });
|
|
97
98
|
var rateLimit_1 = require("./middleware/rateLimit");
|
|
98
99
|
Object.defineProperty(exports, "checkRateLimit", { enumerable: true, get: function () { return rateLimit_1.checkRateLimit; } });
|
|
100
|
+
// ── File access ──────────────────────────────────────────────────────────────
|
|
101
|
+
// Parse has no per-file permission; these turn a file's PATH into one. The kit
|
|
102
|
+
// owns the single `beforeFind(Parse.File)` slot so that rules compose instead
|
|
103
|
+
// of silently replacing each other, and nothing is registered until a rule is
|
|
104
|
+
// added — so upgrading changes no behaviour for a project that does not opt in.
|
|
105
|
+
var fileAccess_1 = require("./files/fileAccess");
|
|
106
|
+
Object.defineProperty(exports, "registerFileAccessRule", { enumerable: true, get: function () { return fileAccess_1.registerFileAccessRule; } });
|
|
107
|
+
Object.defineProperty(exports, "getFileAccessRules", { enumerable: true, get: function () { return fileAccess_1.getFileAccessRules; } });
|
|
108
|
+
Object.defineProperty(exports, "clearFileAccessRules", { enumerable: true, get: function () { return fileAccess_1.clearFileAccessRules; } });
|
|
109
|
+
Object.defineProperty(exports, "evaluateFileAccess", { enumerable: true, get: function () { return fileAccess_1.evaluateFileAccess; } });
|
|
110
|
+
Object.defineProperty(exports, "applyFileAccessHeaders", { enumerable: true, get: function () { return fileAccess_1.applyFileAccessHeaders; } });
|
|
111
|
+
Object.defineProperty(exports, "privateFileRule", { enumerable: true, get: function () { return fileAccess_1.privateFileRule; } });
|
|
112
|
+
Object.defineProperty(exports, "attachPrivateFile", { enumerable: true, get: function () { return fileAccess_1.attachPrivateFile; } });
|
|
113
|
+
Object.defineProperty(exports, "isPrivatePath", { enumerable: true, get: function () { return fileAccess_1.isPrivatePath; } });
|
|
114
|
+
Object.defineProperty(exports, "privateOwnerOf", { enumerable: true, get: function () { return fileAccess_1.privateOwnerOf; } });
|
|
115
|
+
Object.defineProperty(exports, "privateDirectoryFor", { enumerable: true, get: function () { return fileAccess_1.privateDirectoryFor; } });
|
|
116
|
+
Object.defineProperty(exports, "PRIVATE_PREFIX", { enumerable: true, get: function () { return fileAccess_1.PRIVATE_PREFIX; } });
|
|
99
117
|
// ── Database ──
|
|
100
118
|
var schema_1 = require("./database/schema");
|
|
101
119
|
Object.defineProperty(exports, "createSchemaConfig", { enumerable: true, get: function () { return schema_1.createSchemaConfig; } });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,+DAA+D
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
* Returns null if allowed, or an error response object if exceeded.
|
|
5
|
-
*/
|
|
6
|
-
export declare function checkRateLimit(req: Request, res: Response, functionName: string, config: {
|
|
2
|
+
/** Per-function configuration, as declared on `@CloudFunction({rateLimit})`. */
|
|
3
|
+
export interface RateLimitConfig {
|
|
7
4
|
windowMs: number;
|
|
8
5
|
max: number;
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Optional custom bucket key.
|
|
8
|
+
*
|
|
9
|
+
* This middleware runs BEFORE Parse Server validates the session, so there is
|
|
10
|
+
* no authenticated user here. Supply this only if the app can derive a
|
|
11
|
+
* trustworthy identity on its own (e.g. a validated API key). Returning
|
|
12
|
+
* anything the caller controls freely re-opens the bypass described below.
|
|
13
|
+
*/
|
|
14
|
+
keyBy?: (req: Request) => string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check the rate limit for a function + client.
|
|
18
|
+
*
|
|
19
|
+
* Returns true when the request may proceed. Returns false after sending a 429,
|
|
20
|
+
* in which case the caller must not continue the chain.
|
|
21
|
+
*
|
|
22
|
+
* NOTE — the store is in-memory and therefore PER PROCESS. With N app instances
|
|
23
|
+
* the effective limit is N x `max`. For cross-instance enforcement use Parse
|
|
24
|
+
* Server's own `rateLimit` server option with `redisUrl`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkRateLimit(req: Request, res: Response, functionName: string, config: RateLimitConfig): boolean;
|
|
27
|
+
/** Clear all buckets. Intended for tests. */
|
|
28
|
+
export declare function resetRateLimits(): void;
|
|
10
29
|
//# sourceMappingURL=rateLimit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAO1C,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA8DD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,eAAe,GACtB,OAAO,CAwCT;AAED,6CAA6C;AAC7C,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkRateLimit = checkRateLimit;
|
|
4
|
+
exports.resetRateLimits = resetRateLimits;
|
|
4
5
|
// Map<functionName, Map<clientKey, RateLimitEntry>>
|
|
5
6
|
const store = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Hard ceiling on tracked clients per function. A caller rotating addresses
|
|
9
|
+
* would otherwise grow this map without bound — turning the rate limiter into
|
|
10
|
+
* a memory-exhaustion vector. When the ceiling is hit we drop the oldest
|
|
11
|
+
* entries rather than stop limiting.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_CLIENTS_PER_FUNCTION = 10000;
|
|
6
14
|
// Cleanup old entries every 5 minutes
|
|
7
15
|
const cleanupTimer = setInterval(() => {
|
|
8
16
|
const now = Date.now();
|
|
@@ -24,34 +32,68 @@ const cleanupTimer = setInterval(() => {
|
|
|
24
32
|
// which is the only time it has anything to do.
|
|
25
33
|
cleanupTimer.unref();
|
|
26
34
|
/**
|
|
27
|
-
*
|
|
35
|
+
* Resolve the bucket key for a request.
|
|
36
|
+
*
|
|
37
|
+
* SECURITY — why this is the client address and NOT the session token:
|
|
38
|
+
*
|
|
39
|
+
* Until v2.11.0 the key was `req.headers['x-parse-session-token'] || req.ip`.
|
|
40
|
+
* That header is supplied by the caller and is NOT validated at this point in
|
|
41
|
+
* the pipeline, so sending a fresh random token on every request minted a fresh
|
|
42
|
+
* bucket each time and the limit did nothing at all. No account was needed.
|
|
43
|
+
*
|
|
44
|
+
* The address is the only identifier available here that the caller cannot
|
|
45
|
+
* multiply at will.
|
|
46
|
+
*
|
|
47
|
+
* `req.ip` honours Express's `trust proxy` setting. The app MUST configure that
|
|
48
|
+
* precisely — a hop count, or the proxy's address. With a permissive value
|
|
49
|
+
* (`true`) a caller sets `X-Forwarded-For: 127.0.0.1` and spoofs the key, which
|
|
50
|
+
* is the same bypass Parse Server fixed in 9.10.1 for its own limiter. When
|
|
51
|
+
* `trust proxy` is unset, Express already ignores the header and `req.ip` is
|
|
52
|
+
* the socket address, which is what we want.
|
|
28
53
|
*/
|
|
29
|
-
function getClientKey(req) {
|
|
30
|
-
|
|
54
|
+
function getClientKey(req, config) {
|
|
55
|
+
if (config.keyBy) {
|
|
56
|
+
return config.keyBy(req);
|
|
57
|
+
}
|
|
58
|
+
return req.ip || req.socket?.remoteAddress || 'unknown';
|
|
31
59
|
}
|
|
32
60
|
/**
|
|
33
|
-
* Check rate limit for a function + client.
|
|
34
|
-
*
|
|
61
|
+
* Check the rate limit for a function + client.
|
|
62
|
+
*
|
|
63
|
+
* Returns true when the request may proceed. Returns false after sending a 429,
|
|
64
|
+
* in which case the caller must not continue the chain.
|
|
65
|
+
*
|
|
66
|
+
* NOTE — the store is in-memory and therefore PER PROCESS. With N app instances
|
|
67
|
+
* the effective limit is N x `max`. For cross-instance enforcement use Parse
|
|
68
|
+
* Server's own `rateLimit` server option with `redisUrl`.
|
|
35
69
|
*/
|
|
36
70
|
function checkRateLimit(req, res, functionName, config) {
|
|
37
|
-
const clientKey = getClientKey(req);
|
|
71
|
+
const clientKey = getClientKey(req, config);
|
|
38
72
|
const now = Date.now();
|
|
39
|
-
// Get or create function map
|
|
40
73
|
if (!store.has(functionName)) {
|
|
41
74
|
store.set(functionName, new Map());
|
|
42
75
|
}
|
|
43
76
|
const clients = store.get(functionName);
|
|
44
|
-
// Get or create client entry
|
|
45
77
|
const entry = clients.get(clientKey);
|
|
46
78
|
if (!entry || now > entry.resetAt) {
|
|
47
|
-
// First request
|
|
79
|
+
// First request in this window — start fresh.
|
|
80
|
+
if (!entry && clients.size >= MAX_CLIENTS_PER_FUNCTION) {
|
|
81
|
+
// Evict the oldest-expiring entries to make room. Map preserves insertion
|
|
82
|
+
// order, so the head is the least recently added.
|
|
83
|
+
const toEvict = Math.ceil(MAX_CLIENTS_PER_FUNCTION * 0.1);
|
|
84
|
+
let evicted = 0;
|
|
85
|
+
for (const key of clients.keys()) {
|
|
86
|
+
clients.delete(key);
|
|
87
|
+
if (++evicted >= toEvict)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
48
91
|
clients.set(clientKey, { count: 1, resetAt: now + config.windowMs });
|
|
49
|
-
return true;
|
|
92
|
+
return true;
|
|
50
93
|
}
|
|
51
94
|
if (entry.count < config.max) {
|
|
52
|
-
// Under limit
|
|
53
95
|
entry.count++;
|
|
54
|
-
return true;
|
|
96
|
+
return true;
|
|
55
97
|
}
|
|
56
98
|
// Exceeded — send 429
|
|
57
99
|
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
|
|
@@ -60,6 +102,10 @@ function checkRateLimit(req, res, functionName, config) {
|
|
|
60
102
|
message: `Too many requests. Try again in ${retryAfter} seconds.`,
|
|
61
103
|
retryAfter,
|
|
62
104
|
});
|
|
63
|
-
return false;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/** Clear all buckets. Intended for tests. */
|
|
108
|
+
function resetRateLimits() {
|
|
109
|
+
store.clear();
|
|
64
110
|
}
|
|
65
111
|
//# sourceMappingURL=rateLimit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@90soft/parse-server-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Complete decorator-driven development kit for Parse Server — models, cloud functions, triggers, cron, routing, ACL, Swagger, and more.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"url": "https://git.90-soft.com/90_soft/parse-server-kit.git"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
37
|
+
"node": ">=20.19.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@types/parse": ">=3.0.0",
|
|
41
41
|
"express": ">=4.0.0",
|
|
42
42
|
"node-cron": ">=3.0.0",
|
|
43
|
-
"parse": ">=
|
|
44
|
-
"parse-server": ">=
|
|
43
|
+
"parse": ">=8.0.0",
|
|
44
|
+
"parse-server": ">=9.0.0",
|
|
45
45
|
"reflect-metadata": ">=0.1.13",
|
|
46
46
|
"swagger-ui-express": ">=4.0.0"
|
|
47
47
|
},
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"jest": "^29.7.0",
|
|
68
68
|
"mongodb-memory-server": "^11.2.0",
|
|
69
69
|
"node-cron": "^4.0.0",
|
|
70
|
-
"parse": "^
|
|
71
|
-
"parse-server": "^9.10.
|
|
70
|
+
"parse": "^8.6.2",
|
|
71
|
+
"parse-server": "^9.10.4",
|
|
72
72
|
"reflect-metadata": "^0.2.2",
|
|
73
73
|
"swagger-ui-express": "^5.0.1",
|
|
74
74
|
"ts-jest": "^29.2.0",
|