@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 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). Rate limiting is **per process** — no
461
- enforcement across instances.
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 exist
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 | ≥ 18 for this package; **≥ 20.19** with parse-server 9 |
692
- | parse-server | ≥ 8.3 (optional peer) — `keepUnknownIndexes`, adapter internals |
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 | ≥ 5; verified on 5.3.0 and 8.6.0 |
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;IAC9C,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"}
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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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.setupSwagger = exports.getSwaggerJson = 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.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;
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":"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"}
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
- * Check rate limit for a function + client.
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
- }): boolean;
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;AAuC1C;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAiCT"}
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
- * Get client key — session token if available, fallback to IP.
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
- return req.headers['x-parse-session-token'] || req.ip || 'unknown';
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
- * Returns null if allowed, or an error response object if exceeded.
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 or window expired — start fresh
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; // allowed
92
+ return true;
50
93
  }
51
94
  if (entry.count < config.max) {
52
- // Under limit
53
95
  entry.count++;
54
- return true; // allowed
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; // blocked
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":";;AA2CA,wCAsCC;AA1ED,oDAAoD;AACpD,MAAM,KAAK,GAA6C,IAAI,GAAG,EAAE,CAAC;AAElE,sCAAsC;AACtC,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;QACtC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;YACnC,IAAI,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;gBACxB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;AACH,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAElB,0EAA0E;AAC1E,8EAA8E;AAC9E,0EAA0E;AAC1E,gFAAgF;AAChF,gDAAgD;AAChD,YAAY,CAAC,KAAK,EAAE,CAAC;AAErB;;GAEG;AACH,SAAS,YAAY,CAAC,GAAY;IAChC,OAAQ,GAAG,CAAC,OAAO,CAAC,uBAAuB,CAAY,IAAI,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC;AACjF,CAAC;AAED;;;GAGG;AACH,SAAgB,cAAc,CAC5B,GAAY,EACZ,GAAa,EACb,YAAoB,EACpB,MAAyC;IAEzC,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEvB,6BAA6B;IAC7B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAE,CAAC;IAEzC,6BAA6B;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAErC,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;QAClC,gDAAgD;QAChD,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrE,OAAO,IAAI,CAAC,CAAC,UAAU;IACzB,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QAC7B,cAAc;QACd,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,OAAO,IAAI,CAAC,CAAC,UAAU;IACzB,CAAC;IAED,sBAAsB;IACtB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,OAAO,EAAE,mCAAmC,UAAU,WAAW;QACjE,UAAU;KACX,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,CAAC,UAAU;AAC1B,CAAC"}
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.10.1",
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": ">=18.0.0"
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": ">=5.0.0",
44
- "parse-server": ">=8.3.0",
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": "^5.3.0",
71
- "parse-server": "^9.10.0",
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",