@90soft/parse-server-kit 2.13.0 → 2.15.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 +94 -0
- package/dist/files/fileAccess.d.ts +117 -121
- package/dist/files/fileAccess.d.ts.map +1 -1
- package/dist/files/fileAccess.js +140 -150
- package/dist/files/fileAccess.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,100 @@ All notable changes to `@90soft/parse-server-kit`.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [2.15.0]
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`rowAclFileRule` now has two cache windows, and the asymmetry is the point.**
|
|
16
|
+
|
|
17
|
+
2.14.0 used one `maxAge` for every response. Because the ACL decides, clients
|
|
18
|
+
must authenticate EVERY image request — so every image became
|
|
19
|
+
`private, max-age=60`: no shared caching, and a re-download every minute per
|
|
20
|
+
user, each costing a database read and a stream through the application.
|
|
21
|
+
Catalogue images that could be cached for an hour were being refetched on the
|
|
22
|
+
public cadence.
|
|
23
|
+
|
|
24
|
+
Now:
|
|
25
|
+
|
|
26
|
+
| caller | header | why |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| anonymous success | `public, max-age=60` (`publicMaxAge`) | the row is readable by everyone, but a shared cache cannot tell who asked — so the risky window stays short |
|
|
29
|
+
| identified caller | `private, max-age=3600` (`privateMaxAge`) | lives in that one caller's cache alone, and they were allowed to read it a moment ago — so the safe window can afford to be long |
|
|
30
|
+
|
|
31
|
+
Revoking access leaves an identified caller able to re-read their own copy
|
|
32
|
+
for up to `privateMaxAge`. That is a smaller fact than it looks: they have
|
|
33
|
+
already seen the image, and anyone who has seen an image has it on their
|
|
34
|
+
device regardless. Set either to `0` for instant revocation at the price of
|
|
35
|
+
re-downloading.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## [2.14.0]
|
|
40
|
+
|
|
41
|
+
### Changed — BREAKING (file access rules)
|
|
42
|
+
|
|
43
|
+
- **The owning row's ACL is now evaluated at READ time, for every file. The
|
|
44
|
+
prefix and the per-class flag are gone.**
|
|
45
|
+
|
|
46
|
+
2.12.0/2.13.0 marked files governed at UPLOAD time, by storing them under a
|
|
47
|
+
prefix. That cannot work for an application where content moves between
|
|
48
|
+
public and private, and the reason is structural: **storage location is
|
|
49
|
+
permanent, visibility is not.** A file stored while its row was public stays
|
|
50
|
+
world-readable forever, whatever the row later says — and the failure is
|
|
51
|
+
invisible, because the file loads perfectly.
|
|
52
|
+
|
|
53
|
+
Now every file is stored under `<ClassName>/<objectId>/`, so the path always
|
|
54
|
+
names its owner, and permission is decided on each request from that row's
|
|
55
|
+
ACL:
|
|
56
|
+
|
|
57
|
+
| | before | now |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| row made private | needed the bytes re-stored | takes effect on the next request |
|
|
60
|
+
| row made public | needed the bytes re-stored | takes effect immediately |
|
|
61
|
+
| class becomes public | flag flip **+ re-upload every file** | nothing to do |
|
|
62
|
+
| per-row visibility | inexpressible | the normal case |
|
|
63
|
+
|
|
64
|
+
| 2.13.0 | 2.14.0 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `acl/<Class>/<objectId>/` | `<Class>/<objectId>/` |
|
|
67
|
+
| `ACL_PREFIX` | *gone* |
|
|
68
|
+
| `isAclGoverned` | *gone* — every path is governed |
|
|
69
|
+
| `aclOwnerOf` | `fileOwnerOf` |
|
|
70
|
+
| `aclDirectoryFor` | `ownerDirectoryFor` |
|
|
71
|
+
| `attachAclFile` | `attachOwnedFile` |
|
|
72
|
+
| `aclFileRule` | `rowAclFileRule` |
|
|
73
|
+
| `AclFileOwner` | `FileOwner` |
|
|
74
|
+
| `AclFileRuleOptions` | `RowAclFileRuleOptions` |
|
|
75
|
+
|
|
76
|
+
**A path with no identifiable owner is REFUSED**, so files stored before this
|
|
77
|
+
(flat names, or under the old prefix) stop serving until re-uploaded. That is
|
|
78
|
+
deliberate: the alternative is a permanent ungoverned tier that nobody
|
|
79
|
+
remembers.
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`Cache-Control` derived from the caller, not from configuration.** A rule's
|
|
84
|
+
`headers` now receives the access context. `rowAclFileRule` uses it: an
|
|
85
|
+
**anonymous** success proves the row is readable by everyone, so the response
|
|
86
|
+
is `public, max-age=60`; an identified caller gets `private, max-age=60`,
|
|
87
|
+
because from there it cannot be told whether the row is public or restricted
|
|
88
|
+
to them — and a second query to find out is not worth doubling the cost.
|
|
89
|
+
|
|
90
|
+
`maxAge: 0` gives instant revocation at the price of re-downloading, since
|
|
91
|
+
Parse sends no `ETag` and answers no conditional request. The default 60
|
|
92
|
+
bounds less than it appears: a cached copy only ever exists for someone who
|
|
93
|
+
was already allowed to see the file, and anyone who has viewed an image has
|
|
94
|
+
it on their device regardless.
|
|
95
|
+
|
|
96
|
+
### Note
|
|
97
|
+
|
|
98
|
+
- Clients must send `X-Parse-Session-Token` on image requests for any class
|
|
99
|
+
whose CLP is not public — a browser `<img src>` cannot, so protected images
|
|
100
|
+
need fetching and binding. This is the honest cost of the ACL being the
|
|
101
|
+
source of truth, and no design avoids it.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
11
105
|
## [2.13.0]
|
|
12
106
|
|
|
13
107
|
### Changed — BREAKING (file access rules, added in 2.12.0)
|
|
@@ -1,63 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* File access rules —
|
|
2
|
+
* File access rules — the owning row's ACL decides who gets the bytes.
|
|
3
3
|
*
|
|
4
4
|
* Parse has no per-file permission and cannot have one: the file router is
|
|
5
5
|
* handed a name, and a name does not know which row references it. So a file
|
|
6
6
|
* url is a bearer capability — anyone holding the string gets the bytes, with
|
|
7
|
-
* no session, no ACL check and no expiry.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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".
|
|
7
|
+
* no session, no ACL check and no expiry. Fine for a logo. Not fine for a
|
|
8
|
+
* document, a profile photo, or anything a person can make private, because a
|
|
9
|
+
* url that leaks once works forever.
|
|
13
10
|
*
|
|
14
|
-
* Parse
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Parse gives one hook: `beforeFind` on `Parse.File`, which runs on every
|
|
12
|
+
* `GET /files/...` and can throw to refuse. This module owns that hook.
|
|
13
|
+
*
|
|
14
|
+
* **Every file is stored under `<ClassName>/<objectId>/`**, so the path always
|
|
15
|
+
* names the row that owns it, and permission is decided **at read time** from
|
|
16
|
+
* that row's ACL. That matters more than it sounds:
|
|
17
|
+
*
|
|
18
|
+
* - a row made private takes its file out of service on the NEXT request;
|
|
19
|
+
* - a row made public starts serving immediately;
|
|
20
|
+
* - neither needs the bytes re-stored, re-named or migrated.
|
|
21
|
+
*
|
|
22
|
+
* The alternative — marking files private *when they are uploaded* — cannot do
|
|
23
|
+
* either. Storage location is permanent and visibility is not, so an
|
|
24
|
+
* upload-time decision is wrong the moment anything changes, and wrong in the
|
|
25
|
+
* fail-open direction: a file stored while its row was public stays
|
|
26
|
+
* world-readable forever, whatever the row later says. For an application where
|
|
27
|
+
* content moves between public and private, that design is not merely weaker,
|
|
28
|
+
* it is unable to express the requirement.
|
|
29
|
+
*
|
|
30
|
+
* **Five traps this absorbs**, each of which cost real debugging:
|
|
17
31
|
*
|
|
18
32
|
* 1. **There is exactly ONE slot.** File triggers are keyed on the
|
|
19
|
-
* pseudo-class `@File`,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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:
|
|
33
|
+
* pseudo-class `@File`, so a second `beforeFind(Parse.File)` REPLACES the
|
|
34
|
+
* first — Parse logs "Only the last one will be used" and keeps serving.
|
|
35
|
+
* Two features each registering a rule would silently disable one. Hence
|
|
36
|
+
* a registry: this module holds the slot, rules compose.
|
|
37
|
+
* 2. **Response headers come from `afterFind`.** `beforeFind`'s are
|
|
38
|
+
* discarded. A rule that controls caching has to act there.
|
|
39
|
+
* 3. **Only a header authenticates.** The file route reads the session from
|
|
40
|
+
* `X-Parse-Session-Token` and nowhere else — no cookie, no query
|
|
41
|
+
* parameter. A browser `<img src>` cannot send one, so a client must
|
|
42
|
+
* fetch protected images itself and bind the bytes. The upside: nobody
|
|
43
|
+
* can "simplify" this by putting a session token in a url.
|
|
44
|
+
* 4. **A throw is global.** This runs on every file download in the
|
|
45
|
+
* application, so a mistake does not break documents — it blanks every
|
|
46
|
+
* image at once. A rule whose predicate throws is skipped, not fatal.
|
|
47
|
+
* 5. **Fail-open is the invisible failure.** A path whose owner cannot be
|
|
48
|
+
* identified must be REFUSED. Treating it as public means the file loads
|
|
49
|
+
* perfectly and nobody ever notices.
|
|
50
|
+
*
|
|
51
|
+
* **Nothing is registered until a rule is added**, so upgrading changes no
|
|
52
|
+
* behaviour in a project that has not opted in.
|
|
45
53
|
*
|
|
46
54
|
* ```ts
|
|
47
|
-
*
|
|
48
|
-
*
|
|
55
|
+
* registerFileAccessRule(rowAclFileRule());
|
|
56
|
+
* await attachOwnedFile(row, 'document', upload); // -> Row/<objectId>/...
|
|
49
57
|
* ```
|
|
50
|
-
*
|
|
51
|
-
* [aclFileRule] is the general case: a file stored under
|
|
52
|
-
* `acl/<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
58
|
*/
|
|
56
59
|
/**
|
|
57
60
|
* What a FILE trigger is handed, which is not what a class trigger gets: there
|
|
58
61
|
* is no `object` and no `query`, only the file.
|
|
59
62
|
*
|
|
60
|
-
* Declared
|
|
63
|
+
* Declared here rather than taken from the ambient Parse types on purpose:
|
|
61
64
|
* `@types/parse` does not describe file triggers at all, so a consumer relying
|
|
62
65
|
* on it could not name the argument of [evaluateFileAccess] — a published
|
|
63
66
|
* library should not require its callers to declare types for its own API.
|
|
@@ -74,7 +77,7 @@ export interface FileTriggerRequest {
|
|
|
74
77
|
}
|
|
75
78
|
/** The decision a rule is asked to make. */
|
|
76
79
|
export interface FileAccessContext {
|
|
77
|
-
/** The stored path, including
|
|
80
|
+
/** The stored path, including the owner directory. */
|
|
78
81
|
path: string;
|
|
79
82
|
/** The caller, when they sent a session token. */
|
|
80
83
|
user?: Parse.User;
|
|
@@ -92,26 +95,26 @@ export interface FileAccessRule {
|
|
|
92
95
|
* Does this rule govern that path?
|
|
93
96
|
*
|
|
94
97
|
* **Must not throw and should not await.** It is consulted for every file
|
|
95
|
-
* download in the application
|
|
98
|
+
* download in the application.
|
|
96
99
|
*/
|
|
97
100
|
governs(path: string): boolean;
|
|
98
101
|
/**
|
|
99
|
-
* Decide. Throw a `Parse.Error` to refuse.
|
|
102
|
+
* Decide. Throw a `Parse.Error` to refuse; return normally to allow.
|
|
100
103
|
*
|
|
101
|
-
*
|
|
102
|
-
* yes and the request is not master-key.
|
|
104
|
+
* Only called when [governs] matched and the request is not master-key.
|
|
103
105
|
*/
|
|
104
106
|
authorize(context: FileAccessContext): Promise<void> | void;
|
|
105
107
|
/**
|
|
106
|
-
* Headers
|
|
108
|
+
* Headers for a governed response.
|
|
107
109
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
+
* Receives the same context, so a rule can vary caching by caller — see
|
|
111
|
+
* [rowAclFileRule], where an anonymous success is itself the proof that a
|
|
112
|
+
* row is public and therefore safe for a shared cache.
|
|
110
113
|
*/
|
|
111
|
-
headers
|
|
114
|
+
headers?(context: FileAccessContext): Record<string, string>;
|
|
112
115
|
}
|
|
113
116
|
/**
|
|
114
|
-
* The gate.
|
|
117
|
+
* The gate. Ungoverned files leave before anything is awaited.
|
|
115
118
|
*
|
|
116
119
|
* Exported so a project can unit-test its rules without going through HTTP.
|
|
117
120
|
*/
|
|
@@ -135,101 +138,94 @@ export declare function getFileAccessRules(): FileAccessRule[];
|
|
|
135
138
|
/**
|
|
136
139
|
* Drop every rule. For tests.
|
|
137
140
|
*
|
|
138
|
-
* The Parse hooks stay attached —
|
|
139
|
-
*
|
|
141
|
+
* The Parse hooks stay attached — harmless with no rules, since every path is
|
|
142
|
+
* then ungoverned — and re-attaching would hit Parse's duplicate warning.
|
|
140
143
|
*/
|
|
141
144
|
export declare function clearFileAccessRules(): void;
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
* **Named for the mechanism, not the outcome.** It was `private` first, which
|
|
146
|
-
* was wrong in the most confusing way: the commonest case is a PUBLICLY
|
|
147
|
-
* readable row, whose file is then served to everybody — from a path that
|
|
148
|
-
* said "private". The prefix does not mean secret. It means **the owning row
|
|
149
|
-
* decides**, and a public row decides "everyone".
|
|
150
|
-
*
|
|
151
|
-
* Renamed before anything depended on it, because the prefix is baked into
|
|
152
|
-
* stored filenames: changing it later orphans every file already written.
|
|
153
|
-
*
|
|
154
|
-
* Parse's own `validateDirectory` reserves `metadata` and requires a directory
|
|
155
|
-
* to match `^[a-zA-Z0-9][a-zA-Z0-9_\-/]*$`, so a single lowercase word is safe.
|
|
156
|
-
* It is baked into stored file names, so changing it would orphan every file
|
|
157
|
-
* already written under the old one.
|
|
158
|
-
*/
|
|
159
|
-
export declare const ACL_PREFIX = "acl";
|
|
160
|
-
/** Who an ACL-governed file's path says owns it. */
|
|
161
|
-
export interface AclFileOwner {
|
|
145
|
+
/** Who a stored path says owns the bytes. */
|
|
146
|
+
export interface FileOwner {
|
|
162
147
|
className: string;
|
|
163
148
|
objectId: string;
|
|
164
149
|
}
|
|
165
150
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* Deliberately separate from [aclOwnerOf], and the separation is the
|
|
169
|
-
* safety property: "under the prefix" and "parses into an owner" are different
|
|
170
|
-
* questions, and a single predicate returning `owner | null` conflates them
|
|
171
|
-
* into a fail-OPEN. It would answer `null` both for an ordinary image and for
|
|
172
|
-
* `acl/<Class>/<id>/extra/doc.jpg` — one directory deeper than expected —
|
|
173
|
-
* and so serve that document to anybody.
|
|
174
|
-
*/
|
|
175
|
-
export declare function isAclGoverned(path: string): boolean;
|
|
176
|
-
/**
|
|
177
|
-
* Read the owner out of a governed path, or null if it is not well formed.
|
|
151
|
+
* Read the owner out of a stored path, or null if the path has no owner.
|
|
178
152
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
153
|
+
* **Must never throw.** It runs on every file download in the application, and
|
|
154
|
+
* a throw inside the file trigger does not break one feature — it blanks every
|
|
155
|
+
* image at once. Hence a plain `split` and nothing clever.
|
|
156
|
+
*
|
|
157
|
+
* `null` means "this file predates owner directories, or was stored by
|
|
158
|
+
* something else". A rule must treat that as a REFUSAL rather than as
|
|
159
|
+
* permission; see the fail-open note in the module docs.
|
|
181
160
|
*/
|
|
182
|
-
export declare function
|
|
161
|
+
export declare function fileOwnerOf(path: string): FileOwner | null;
|
|
183
162
|
/**
|
|
184
|
-
* The directory a row's
|
|
163
|
+
* The directory a row's files live in.
|
|
185
164
|
*
|
|
186
|
-
* Throws rather than quietly producing something Parse
|
|
187
|
-
*
|
|
165
|
+
* Throws rather than quietly producing something Parse rejects: a class whose
|
|
166
|
+
* name cannot be a path segment (`_User`, since a directory must start
|
|
188
167
|
* alphanumeric) surfaces here, at the one call site, instead of as a confusing
|
|
189
168
|
* upload failure later.
|
|
190
169
|
*/
|
|
191
|
-
export declare function
|
|
170
|
+
export declare function ownerDirectoryFor(className: string, objectId: string): string;
|
|
192
171
|
/**
|
|
193
172
|
* Store a file so that the owning row's ACL governs it.
|
|
194
173
|
*
|
|
195
174
|
* The row must already be saved — its objectId is part of the path, which is
|
|
196
|
-
* what lets
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* **This should be the only way an application stores a governed file.** The
|
|
201
|
-
* prefix is the entire boundary: a document saved without it is simply public,
|
|
202
|
-
* and nothing anywhere will complain.
|
|
175
|
+
* what lets a rule find the owner with no index and no second source of truth.
|
|
176
|
+
* For a new record that means save, attach, save again: one extra write on a
|
|
177
|
+
* rare operation.
|
|
203
178
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* travels inside DTOs where the document itself does not.
|
|
179
|
+
* **This should be the only way an application stores a file.** A file stored
|
|
180
|
+
* without an owner directory is ungoverned, and nothing anywhere complains.
|
|
207
181
|
*/
|
|
208
|
-
export declare function
|
|
182
|
+
export declare function attachOwnedFile(row: Parse.Object, field: string, upload: {
|
|
209
183
|
base64: string;
|
|
210
184
|
name?: string;
|
|
211
185
|
}): Promise<void>;
|
|
212
|
-
/** Options for [
|
|
213
|
-
export interface
|
|
186
|
+
/** Options for [rowAclFileRule]. */
|
|
187
|
+
export interface RowAclFileRuleOptions {
|
|
214
188
|
/**
|
|
215
|
-
*
|
|
189
|
+
* How long a SHARED cache may hold a publicly readable file, in seconds.
|
|
190
|
+
* Default 60.
|
|
191
|
+
*
|
|
192
|
+
* Only ever applied when an ANONYMOUS caller succeeded, which proves the row
|
|
193
|
+
* is readable by everyone. Short, because this is the one window where a
|
|
194
|
+
* revoked file could still be served to somebody who never had access: a
|
|
195
|
+
* shared cache does not know who asked.
|
|
196
|
+
*/
|
|
197
|
+
publicMaxAge?: number;
|
|
198
|
+
/**
|
|
199
|
+
* How long the CALLER'S OWN cache may hold a file, in seconds. Default 3600.
|
|
200
|
+
*
|
|
201
|
+
* Longer than `publicMaxAge` on purpose, and the asymmetry is the point. This
|
|
202
|
+
* response is marked `private`, so it lives in one identified caller's cache
|
|
203
|
+
* and in no shared one — and that caller was, by definition, allowed to read
|
|
204
|
+
* the file a moment ago. Revoking access leaves them able to re-read their
|
|
205
|
+
* own copy for up to this long, which is a far smaller fact than it appears:
|
|
206
|
+
* they have already seen the image, and anyone who has seen an image has it
|
|
207
|
+
* on their device regardless.
|
|
216
208
|
*
|
|
217
|
-
*
|
|
209
|
+
* It matters because clients authenticate every image request — they must,
|
|
210
|
+
* since the ACL decides — so WITHOUT this every image in a signed-in app
|
|
211
|
+
* would re-download on the short public window, each time costing a database
|
|
212
|
+
* read and a stream through the application. Measured consequence of getting
|
|
213
|
+
* it wrong: catalogue images that could be cached for an hour re-fetching
|
|
214
|
+
* every minute, per user.
|
|
218
215
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* the strict value rather than being left to the caller.
|
|
216
|
+
* `0` disables caching entirely for identified callers: instant revocation,
|
|
217
|
+
* every view re-downloads.
|
|
222
218
|
*/
|
|
223
|
-
|
|
219
|
+
privateMaxAge?: number;
|
|
224
220
|
}
|
|
225
221
|
/**
|
|
226
|
-
* Serve a
|
|
222
|
+
* Serve a file only to a caller who can read the row that owns it.
|
|
227
223
|
*
|
|
228
|
-
* The caller is resolved from their session token, so Parse evaluates the
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
224
|
+
* The caller is resolved from their session token, so Parse evaluates the row's
|
|
225
|
+
* ACL and the class's CLP exactly as it would for a query. With no session the
|
|
226
|
+
* row is loaded with no auth, which is right rather than a shortcut: if the
|
|
227
|
+
* owning row is publicly readable then its file is public too, because the
|
|
228
|
+
* row's ACL is the whole rule.
|
|
233
229
|
*/
|
|
234
|
-
export declare function
|
|
230
|
+
export declare function rowAclFileRule(options?: RowAclFileRuleOptions): FileAccessRule;
|
|
235
231
|
//# sourceMappingURL=fileAccess.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileAccess.d.ts","sourceRoot":"","sources":["../../src/files/fileAccess.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fileAccess.d.ts","sourceRoot":"","sources":["../../src/files/fileAccess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;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,sDAAsD;IACtD,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;;;;OAIG;IACH,SAAS,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE5D;;;;;;OAMG;IACH,OAAO,CAAC,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9D;AAuCD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAYxE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAqBjE;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,IAAI,cAAc,EAAE,CAErD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAID,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAW1D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc7E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,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,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAkDhB"}
|
package/dist/files/fileAccess.js
CHANGED
|
@@ -1,71 +1,72 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* File access rules —
|
|
3
|
+
* File access rules — the owning row's ACL decides who gets the bytes.
|
|
4
4
|
*
|
|
5
5
|
* Parse has no per-file permission and cannot have one: the file router is
|
|
6
6
|
* handed a name, and a name does not know which row references it. So a file
|
|
7
7
|
* url is a bearer capability — anyone holding the string gets the bytes, with
|
|
8
|
-
* no session, no ACL check and no expiry.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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".
|
|
8
|
+
* no session, no ACL check and no expiry. Fine for a logo. Not fine for a
|
|
9
|
+
* document, a profile photo, or anything a person can make private, because a
|
|
10
|
+
* url that leaks once works forever.
|
|
14
11
|
*
|
|
15
|
-
* Parse
|
|
16
|
-
*
|
|
17
|
-
* and makes it usable, because using it directly has five traps:
|
|
12
|
+
* Parse gives one hook: `beforeFind` on `Parse.File`, which runs on every
|
|
13
|
+
* `GET /files/...` and can throw to refuse. This module owns that hook.
|
|
18
14
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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.
|
|
15
|
+
* **Every file is stored under `<ClassName>/<objectId>/`**, so the path always
|
|
16
|
+
* names the row that owns it, and permission is decided **at read time** from
|
|
17
|
+
* that row's ACL. That matters more than it sounds:
|
|
18
|
+
*
|
|
19
|
+
* - a row made private takes its file out of service on the NEXT request;
|
|
20
|
+
* - a row made public starts serving immediately;
|
|
21
|
+
* - neither needs the bytes re-stored, re-named or migrated.
|
|
22
|
+
*
|
|
23
|
+
* The alternative — marking files private *when they are uploaded* — cannot do
|
|
24
|
+
* either. Storage location is permanent and visibility is not, so an
|
|
25
|
+
* upload-time decision is wrong the moment anything changes, and wrong in the
|
|
26
|
+
* fail-open direction: a file stored while its row was public stays
|
|
27
|
+
* world-readable forever, whatever the row later says. For an application where
|
|
28
|
+
* content moves between public and private, that design is not merely weaker,
|
|
29
|
+
* it is unable to express the requirement.
|
|
30
|
+
*
|
|
31
|
+
* **Five traps this absorbs**, each of which cost real debugging:
|
|
40
32
|
*
|
|
41
|
-
* **
|
|
42
|
-
*
|
|
43
|
-
*
|
|
33
|
+
* 1. **There is exactly ONE slot.** File triggers are keyed on the
|
|
34
|
+
* pseudo-class `@File`, so a second `beforeFind(Parse.File)` REPLACES the
|
|
35
|
+
* first — Parse logs "Only the last one will be used" and keeps serving.
|
|
36
|
+
* Two features each registering a rule would silently disable one. Hence
|
|
37
|
+
* a registry: this module holds the slot, rules compose.
|
|
38
|
+
* 2. **Response headers come from `afterFind`.** `beforeFind`'s are
|
|
39
|
+
* discarded. A rule that controls caching has to act there.
|
|
40
|
+
* 3. **Only a header authenticates.** The file route reads the session from
|
|
41
|
+
* `X-Parse-Session-Token` and nowhere else — no cookie, no query
|
|
42
|
+
* parameter. A browser `<img src>` cannot send one, so a client must
|
|
43
|
+
* fetch protected images itself and bind the bytes. The upside: nobody
|
|
44
|
+
* can "simplify" this by putting a session token in a url.
|
|
45
|
+
* 4. **A throw is global.** This runs on every file download in the
|
|
46
|
+
* application, so a mistake does not break documents — it blanks every
|
|
47
|
+
* image at once. A rule whose predicate throws is skipped, not fatal.
|
|
48
|
+
* 5. **Fail-open is the invisible failure.** A path whose owner cannot be
|
|
49
|
+
* identified must be REFUSED. Treating it as public means the file loads
|
|
50
|
+
* perfectly and nobody ever notices.
|
|
44
51
|
*
|
|
45
|
-
*
|
|
52
|
+
* **Nothing is registered until a rule is added**, so upgrading changes no
|
|
53
|
+
* behaviour in a project that has not opted in.
|
|
46
54
|
*
|
|
47
55
|
* ```ts
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* registerFileAccessRule(rowAclFileRule());
|
|
57
|
+
* await attachOwnedFile(row, 'document', upload); // -> Row/<objectId>/...
|
|
50
58
|
* ```
|
|
51
|
-
*
|
|
52
|
-
* [aclFileRule] is the general case: a file stored under
|
|
53
|
-
* `acl/<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
59
|
*/
|
|
57
60
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
-
exports.ACL_PREFIX = void 0;
|
|
59
61
|
exports.evaluateFileAccess = evaluateFileAccess;
|
|
60
62
|
exports.applyFileAccessHeaders = applyFileAccessHeaders;
|
|
61
63
|
exports.registerFileAccessRule = registerFileAccessRule;
|
|
62
64
|
exports.getFileAccessRules = getFileAccessRules;
|
|
63
65
|
exports.clearFileAccessRules = clearFileAccessRules;
|
|
64
|
-
exports.
|
|
65
|
-
exports.
|
|
66
|
-
exports.
|
|
67
|
-
exports.
|
|
68
|
-
exports.aclFileRule = aclFileRule;
|
|
66
|
+
exports.fileOwnerOf = fileOwnerOf;
|
|
67
|
+
exports.ownerDirectoryFor = ownerDirectoryFor;
|
|
68
|
+
exports.attachOwnedFile = attachOwnedFile;
|
|
69
|
+
exports.rowAclFileRule = rowAclFileRule;
|
|
69
70
|
/** Ordered; the first rule that governs a path decides it. */
|
|
70
71
|
const rules = [];
|
|
71
72
|
/** The hook is attached once, lazily, on the first registration. */
|
|
@@ -74,17 +75,15 @@ let attached = false;
|
|
|
74
75
|
* 404, never 403.
|
|
75
76
|
*
|
|
76
77
|
* A 403 confirms the file is there and merely forbidden, which for a document
|
|
77
|
-
* is itself the leak
|
|
78
|
-
* "Not found" is the same answer an invented name would get.
|
|
78
|
+
* is itself the leak. "Not found" is the answer an invented name would get.
|
|
79
79
|
*/
|
|
80
80
|
function notFound() {
|
|
81
81
|
return new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'File not found');
|
|
82
82
|
}
|
|
83
|
-
/** The first rule governing this path, or undefined when
|
|
83
|
+
/** The first rule governing this path, or undefined when none does. */
|
|
84
84
|
function ruleFor(path) {
|
|
85
85
|
for (const rule of rules) {
|
|
86
|
-
//
|
|
87
|
-
// misbehaving rule must not take the public path down with it.
|
|
86
|
+
// One misbehaving rule must not blank every image in the app.
|
|
88
87
|
try {
|
|
89
88
|
if (rule.governs(path))
|
|
90
89
|
return rule;
|
|
@@ -95,25 +94,28 @@ function ruleFor(path) {
|
|
|
95
94
|
}
|
|
96
95
|
return undefined;
|
|
97
96
|
}
|
|
97
|
+
function contextFor(request) {
|
|
98
|
+
return {
|
|
99
|
+
path: request.file?.name() ?? '',
|
|
100
|
+
user: request.user,
|
|
101
|
+
master: request.master === true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
98
104
|
/**
|
|
99
|
-
* The gate.
|
|
105
|
+
* The gate. Ungoverned files leave before anything is awaited.
|
|
100
106
|
*
|
|
101
107
|
* Exported so a project can unit-test its rules without going through HTTP.
|
|
102
108
|
*/
|
|
103
109
|
async function evaluateFileAccess(request) {
|
|
104
|
-
const
|
|
105
|
-
const rule = ruleFor(path);
|
|
110
|
+
const context = contextFor(request);
|
|
111
|
+
const rule = ruleFor(context.path);
|
|
106
112
|
if (!rule)
|
|
107
113
|
return;
|
|
108
|
-
// Cloud code reading deliberately
|
|
109
|
-
//
|
|
110
|
-
if (
|
|
114
|
+
// Cloud code reading deliberately. NOT how a worker that fetches a url
|
|
115
|
+
// reaches a file — a plain GET carries no key.
|
|
116
|
+
if (context.master)
|
|
111
117
|
return;
|
|
112
|
-
await rule.authorize(
|
|
113
|
-
path,
|
|
114
|
-
user: request.user,
|
|
115
|
-
master: false,
|
|
116
|
-
});
|
|
118
|
+
await rule.authorize(context);
|
|
117
119
|
}
|
|
118
120
|
/**
|
|
119
121
|
* Add the governed response's headers.
|
|
@@ -122,12 +124,16 @@ async function evaluateFileAccess(request) {
|
|
|
122
124
|
* the `afterFind` request and ignores `beforeFind`'s.
|
|
123
125
|
*/
|
|
124
126
|
function applyFileAccessHeaders(request) {
|
|
125
|
-
const
|
|
127
|
+
const context = contextFor(request);
|
|
128
|
+
const rule = ruleFor(context.path);
|
|
126
129
|
if (!rule?.headers)
|
|
127
130
|
return;
|
|
128
131
|
// The incoming object already carries `X-Content-Type-Options: nosniff`, so
|
|
129
132
|
// extend it rather than replacing it.
|
|
130
|
-
request.responseHeaders = {
|
|
133
|
+
request.responseHeaders = {
|
|
134
|
+
...(request.responseHeaders ?? {}),
|
|
135
|
+
...rule.headers(context),
|
|
136
|
+
};
|
|
131
137
|
}
|
|
132
138
|
/**
|
|
133
139
|
* Register a rule, attaching the Parse hooks on the first call.
|
|
@@ -149,9 +155,8 @@ function registerFileAccessRule(rule) {
|
|
|
149
155
|
attached = true;
|
|
150
156
|
/*
|
|
151
157
|
* `Parse.File` is passed as the class, which Parse maps to `@File`. This is
|
|
152
|
-
* the only pair of triggers in a kit project
|
|
153
|
-
*
|
|
154
|
-
* on.
|
|
158
|
+
* the only pair of triggers in a kit project not declared on a model with
|
|
159
|
+
* `@BeforeFind` / `@AfterFind`, because there is no model to declare it on.
|
|
155
160
|
*/
|
|
156
161
|
Parse.Cloud.beforeFind(Parse.File, evaluateFileAccess);
|
|
157
162
|
Parse.Cloud.afterFind(Parse.File, applyFileAccessHeaders);
|
|
@@ -163,131 +168,94 @@ function getFileAccessRules() {
|
|
|
163
168
|
/**
|
|
164
169
|
* Drop every rule. For tests.
|
|
165
170
|
*
|
|
166
|
-
* The Parse hooks stay attached —
|
|
167
|
-
*
|
|
171
|
+
* The Parse hooks stay attached — harmless with no rules, since every path is
|
|
172
|
+
* then ungoverned — and re-attaching would hit Parse's duplicate warning.
|
|
168
173
|
*/
|
|
169
174
|
function clearFileAccessRules() {
|
|
170
175
|
rules.length = 0;
|
|
171
176
|
}
|
|
172
|
-
// ── The ACL-governed rule ───────────────────────────────────────────────────
|
|
173
177
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* **Named for the mechanism, not the outcome.** It was `private` first, which
|
|
177
|
-
* was wrong in the most confusing way: the commonest case is a PUBLICLY
|
|
178
|
-
* readable row, whose file is then served to everybody — from a path that
|
|
179
|
-
* said "private". The prefix does not mean secret. It means **the owning row
|
|
180
|
-
* decides**, and a public row decides "everyone".
|
|
178
|
+
* Read the owner out of a stored path, or null if the path has no owner.
|
|
181
179
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
180
|
+
* **Must never throw.** It runs on every file download in the application, and
|
|
181
|
+
* a throw inside the file trigger does not break one feature — it blanks every
|
|
182
|
+
* image at once. Hence a plain `split` and nothing clever.
|
|
184
183
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* already written under the old one.
|
|
184
|
+
* `null` means "this file predates owner directories, or was stored by
|
|
185
|
+
* something else". A rule must treat that as a REFUSAL rather than as
|
|
186
|
+
* permission; see the fail-open note in the module docs.
|
|
189
187
|
*/
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
* Is this path governed at all?
|
|
193
|
-
*
|
|
194
|
-
* Deliberately separate from [aclOwnerOf], and the separation is the
|
|
195
|
-
* safety property: "under the prefix" and "parses into an owner" are different
|
|
196
|
-
* questions, and a single predicate returning `owner | null` conflates them
|
|
197
|
-
* into a fail-OPEN. It would answer `null` both for an ordinary image and for
|
|
198
|
-
* `acl/<Class>/<id>/extra/doc.jpg` — one directory deeper than expected —
|
|
199
|
-
* and so serve that document to anybody.
|
|
200
|
-
*/
|
|
201
|
-
function isAclGoverned(path) {
|
|
202
|
-
return typeof path === 'string' && path.startsWith(`${exports.ACL_PREFIX}/`);
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Read the owner out of a governed path, or null if it is not well formed.
|
|
206
|
-
*
|
|
207
|
-
* `null` means "I cannot tell who owns this", which the rule treats as a
|
|
208
|
-
* refusal — never as permission.
|
|
209
|
-
*/
|
|
210
|
-
function aclOwnerOf(path) {
|
|
211
|
-
if (!isAclGoverned(path))
|
|
188
|
+
function fileOwnerOf(path) {
|
|
189
|
+
if (typeof path !== 'string' || path === '')
|
|
212
190
|
return null;
|
|
213
|
-
// Exactly:
|
|
191
|
+
// Exactly: ClassName / objectId / filename
|
|
214
192
|
const parts = path.split('/');
|
|
215
|
-
if (parts.length !==
|
|
193
|
+
if (parts.length !== 3)
|
|
216
194
|
return null;
|
|
217
|
-
const [
|
|
195
|
+
const [className, objectId, filename] = parts;
|
|
218
196
|
if (!className || !objectId || !filename)
|
|
219
197
|
return null;
|
|
220
198
|
return { className, objectId };
|
|
221
199
|
}
|
|
222
200
|
/**
|
|
223
|
-
* The directory a row's
|
|
201
|
+
* The directory a row's files live in.
|
|
224
202
|
*
|
|
225
|
-
* Throws rather than quietly producing something Parse
|
|
226
|
-
*
|
|
203
|
+
* Throws rather than quietly producing something Parse rejects: a class whose
|
|
204
|
+
* name cannot be a path segment (`_User`, since a directory must start
|
|
227
205
|
* alphanumeric) surfaces here, at the one call site, instead of as a confusing
|
|
228
206
|
* upload failure later.
|
|
229
207
|
*/
|
|
230
|
-
function
|
|
208
|
+
function ownerDirectoryFor(className, objectId) {
|
|
231
209
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_]*$/.test(className)) {
|
|
232
|
-
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${className} cannot own
|
|
210
|
+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${className} cannot own files: Parse requires a directory segment to start with a letter or digit`);
|
|
233
211
|
}
|
|
234
212
|
if (!/^[a-zA-Z0-9]+$/.test(objectId)) {
|
|
235
|
-
throw new Parse.Error(Parse.Error.OTHER_CAUSE, '
|
|
213
|
+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'A file needs its owning row saved first, so that it has an objectId');
|
|
236
214
|
}
|
|
237
|
-
return `${
|
|
215
|
+
return `${className}/${objectId}`;
|
|
238
216
|
}
|
|
239
217
|
/**
|
|
240
218
|
* Store a file so that the owning row's ACL governs it.
|
|
241
219
|
*
|
|
242
220
|
* The row must already be saved — its objectId is part of the path, which is
|
|
243
|
-
* what lets
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* **This should be the only way an application stores a governed file.** The
|
|
248
|
-
* prefix is the entire boundary: a document saved without it is simply public,
|
|
249
|
-
* and nothing anywhere will complain.
|
|
221
|
+
* what lets a rule find the owner with no index and no second source of truth.
|
|
222
|
+
* For a new record that means save, attach, save again: one extra write on a
|
|
223
|
+
* rare operation.
|
|
250
224
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* travels inside DTOs where the document itself does not.
|
|
225
|
+
* **This should be the only way an application stores a file.** A file stored
|
|
226
|
+
* without an owner directory is ungoverned, and nothing anywhere complains.
|
|
254
227
|
*/
|
|
255
|
-
async function
|
|
228
|
+
async function attachOwnedFile(row, field, upload) {
|
|
256
229
|
if (!row.id) {
|
|
257
|
-
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Save the row before attaching a
|
|
230
|
+
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Save the row before attaching a file — its objectId is part of the path');
|
|
258
231
|
}
|
|
259
|
-
const file = new Parse.File(encodeURIComponent(upload.name ?? '
|
|
232
|
+
const file = new Parse.File(encodeURIComponent(upload.name ?? 'file'), {
|
|
260
233
|
base64: upload.base64,
|
|
261
234
|
});
|
|
262
235
|
// Sent as `X-Parse-File-Directory`; honoured by Parse Server from 9.5.0.
|
|
263
|
-
file.setDirectory(
|
|
236
|
+
file.setDirectory(ownerDirectoryFor(row.className, row.id));
|
|
264
237
|
const saved = await file.save({ useMasterKey: true });
|
|
265
238
|
row.set(field, saved);
|
|
266
239
|
}
|
|
267
240
|
/**
|
|
268
|
-
* Serve a
|
|
241
|
+
* Serve a file only to a caller who can read the row that owns it.
|
|
269
242
|
*
|
|
270
|
-
* The caller is resolved from their session token, so Parse evaluates the
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
243
|
+
* The caller is resolved from their session token, so Parse evaluates the row's
|
|
244
|
+
* ACL and the class's CLP exactly as it would for a query. With no session the
|
|
245
|
+
* row is loaded with no auth, which is right rather than a shortcut: if the
|
|
246
|
+
* owning row is publicly readable then its file is public too, because the
|
|
247
|
+
* row's ACL is the whole rule.
|
|
275
248
|
*/
|
|
276
|
-
function
|
|
277
|
-
const
|
|
249
|
+
function rowAclFileRule(options = {}) {
|
|
250
|
+
const publicMaxAge = options.publicMaxAge ?? 60;
|
|
251
|
+
const privateMaxAge = options.privateMaxAge ?? 3600;
|
|
278
252
|
return {
|
|
279
|
-
name: 'acl
|
|
280
|
-
|
|
281
|
-
|
|
253
|
+
name: 'row-acl',
|
|
254
|
+
// Every path. A file with no identifiable owner is REFUSED below, not
|
|
255
|
+
// ignored — ignoring it is the fail-open case.
|
|
256
|
+
governs: () => true,
|
|
282
257
|
async authorize({ path, user }) {
|
|
283
|
-
|
|
284
|
-
* Under the prefix but not a shape we understand: REFUSE.
|
|
285
|
-
*
|
|
286
|
-
* Treating it as public because no owner could be found is the one
|
|
287
|
-
* failure mode of this design that nobody would notice, because the
|
|
288
|
-
* document would load perfectly.
|
|
289
|
-
*/
|
|
290
|
-
const owner = aclOwnerOf(path);
|
|
258
|
+
const owner = fileOwnerOf(path);
|
|
291
259
|
if (!owner)
|
|
292
260
|
throw notFound();
|
|
293
261
|
const sessionToken = user?.getSessionToken();
|
|
@@ -298,6 +266,28 @@ function aclFileRule(options = {}) {
|
|
|
298
266
|
if (!row)
|
|
299
267
|
throw notFound();
|
|
300
268
|
},
|
|
269
|
+
headers({ user }) {
|
|
270
|
+
/*
|
|
271
|
+
* `public` only for an ANONYMOUS success.
|
|
272
|
+
*
|
|
273
|
+
* Reaching here means the row was readable by this caller. If the caller
|
|
274
|
+
* sent no session and still succeeded, the row is readable by everyone,
|
|
275
|
+
* so the bytes are genuinely public and a shared cache may hold them.
|
|
276
|
+
*
|
|
277
|
+
* An identified caller gets `private`: the response may sit in that one
|
|
278
|
+
* user's cache and in no shared one. We cannot tell from here whether
|
|
279
|
+
* their row is public or restricted to them specifically — and a second
|
|
280
|
+
* query to find out is not worth doubling the cost of every image.
|
|
281
|
+
*
|
|
282
|
+
* Hence the two windows. `private` is the safe scope, so it can afford
|
|
283
|
+
* to be long; `public` is the risky one, so it stays short.
|
|
284
|
+
*/
|
|
285
|
+
const scope = user ? 'private' : 'public';
|
|
286
|
+
const maxAge = user ? privateMaxAge : publicMaxAge;
|
|
287
|
+
return {
|
|
288
|
+
'Cache-Control': maxAge > 0 ? `${scope}, max-age=${maxAge}` : `${scope}, no-store`,
|
|
289
|
+
};
|
|
290
|
+
},
|
|
301
291
|
};
|
|
302
292
|
}
|
|
303
293
|
//# sourceMappingURL=fileAccess.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileAccess.js","sourceRoot":"","sources":["../../src/files/fileAccess.ts"],"names":[],"mappings":";AAAA
|
|
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,8 +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,
|
|
28
|
-
export type { FileAccessRule, FileAccessContext, FileTriggerRequest,
|
|
27
|
+
export { registerFileAccessRule, getFileAccessRules, clearFileAccessRules, evaluateFileAccess, applyFileAccessHeaders, rowAclFileRule, attachOwnedFile, fileOwnerOf, ownerDirectoryFor } from './files/fileAccess';
|
|
28
|
+
export type { FileAccessRule, FileAccessContext, FileTriggerRequest, FileOwner, RowAclFileRuleOptions } from './files/fileAccess';
|
|
29
29
|
export { createSchemaConfig } from './database/schema';
|
|
30
30
|
export type { SchemaConfigOptions } from './database/schema';
|
|
31
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;AAOxD,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,oBAAoB,EACvE,kBAAkB,EAAE,sBAAsB,EAC1C,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
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.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.
|
|
6
|
-
exports.setupSwagger = exports.getSwaggerJson = 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.ownerDirectoryFor = exports.fileOwnerOf = exports.attachOwnedFile = exports.rowAclFileRule = 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;
|
|
7
6
|
// ── Decorators ──
|
|
8
7
|
var parseDecorators_1 = require("./decorators/parseDecorators");
|
|
9
8
|
Object.defineProperty(exports, "ParseClass", { enumerable: true, get: function () { return parseDecorators_1.ParseClass; } });
|
|
@@ -97,23 +96,21 @@ Object.defineProperty(exports, "removeResultMiddleware", { enumerable: true, get
|
|
|
97
96
|
Object.defineProperty(exports, "conditionalJsonMiddleware", { enumerable: true, get: function () { return middleware_1.conditionalJsonMiddleware; } });
|
|
98
97
|
var rateLimit_1 = require("./middleware/rateLimit");
|
|
99
98
|
Object.defineProperty(exports, "checkRateLimit", { enumerable: true, get: function () { return rateLimit_1.checkRateLimit; } });
|
|
100
|
-
// ── File access
|
|
101
|
-
// Parse has no per-file permission; these
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
99
|
+
// ── File access ──────────────────────────────────────────────────
|
|
100
|
+
// Parse has no per-file permission; these make the OWNING ROW'S ACL the
|
|
101
|
+
// permission, decided at READ time so a visibility change needs no migration.
|
|
102
|
+
// The kit owns the single `beforeFind(Parse.File)` slot so rules compose, and
|
|
103
|
+
// nothing is registered until a rule is added.
|
|
105
104
|
var fileAccess_1 = require("./files/fileAccess");
|
|
106
105
|
Object.defineProperty(exports, "registerFileAccessRule", { enumerable: true, get: function () { return fileAccess_1.registerFileAccessRule; } });
|
|
107
106
|
Object.defineProperty(exports, "getFileAccessRules", { enumerable: true, get: function () { return fileAccess_1.getFileAccessRules; } });
|
|
108
107
|
Object.defineProperty(exports, "clearFileAccessRules", { enumerable: true, get: function () { return fileAccess_1.clearFileAccessRules; } });
|
|
109
108
|
Object.defineProperty(exports, "evaluateFileAccess", { enumerable: true, get: function () { return fileAccess_1.evaluateFileAccess; } });
|
|
110
109
|
Object.defineProperty(exports, "applyFileAccessHeaders", { enumerable: true, get: function () { return fileAccess_1.applyFileAccessHeaders; } });
|
|
111
|
-
Object.defineProperty(exports, "
|
|
112
|
-
Object.defineProperty(exports, "
|
|
113
|
-
Object.defineProperty(exports, "
|
|
114
|
-
Object.defineProperty(exports, "
|
|
115
|
-
Object.defineProperty(exports, "aclDirectoryFor", { enumerable: true, get: function () { return fileAccess_1.aclDirectoryFor; } });
|
|
116
|
-
Object.defineProperty(exports, "ACL_PREFIX", { enumerable: true, get: function () { return fileAccess_1.ACL_PREFIX; } });
|
|
110
|
+
Object.defineProperty(exports, "rowAclFileRule", { enumerable: true, get: function () { return fileAccess_1.rowAclFileRule; } });
|
|
111
|
+
Object.defineProperty(exports, "attachOwnedFile", { enumerable: true, get: function () { return fileAccess_1.attachOwnedFile; } });
|
|
112
|
+
Object.defineProperty(exports, "fileOwnerOf", { enumerable: true, get: function () { return fileAccess_1.fileOwnerOf; } });
|
|
113
|
+
Object.defineProperty(exports, "ownerDirectoryFor", { enumerable: true, get: function () { return fileAccess_1.ownerDirectoryFor; } });
|
|
117
114
|
// ── Database ──
|
|
118
115
|
var schema_1 = require("./database/schema");
|
|
119
116
|
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":";AAAA,+DAA+D;;;;AAE/D,mBAAmB;AACnB,gEAAiI;AAAxH,6GAAA,UAAU,OAAA;AAAE,6GAAA,UAAU,OAAA;AAAE,sHAAA,mBAAmB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAG1F,8DAA0G;AAAjG,+GAAA,aAAa,OAAA;AAAE,wHAAA,sBAAsB,OAAA;AAAE,sHAAA,oBAAoB,OAAA;AACpE,4DAAmE;AAA1D,sHAAA,qBAAqB,OAAA;AAE9B,kEAKoF;AAL3E,8GAAA,UAAU,OAAA;AAAE,6GAAA,SAAS,OAAA;AAAE,gHAAA,YAAY,OAAA;AAAE,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,6GAAA,SAAS,OAAA;AAC9E,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,+GAAA,WAAW,OAAA;AAAE,8HAAA,0BAA0B,OAAA;AAChE,kHAAA,cAAc,OAAA;AAAE,iHAAA,aAAa,OAAA;AAAE,oHAAA,gBAAgB,OAAA;AAAE,mHAAA,eAAe,OAAA;AAChE,kHAAA,cAAc,OAAA;AAAE,iHAAA,aAAa,OAAA;AAC7B,oHAAA,gBAAgB,OAAA;AAAE,mHAAA,eAAe,OAAA;AACjC,iHAAA,aAAa,OAAA;AAAE,mHAAA,eAAe,OAAA;AAAE,8GAAA,UAAU,OAAA;AAC5C,gEAA+D;AAAtD,kHAAA,eAAe,OAAA;AAExB,4DAA8E;AAArE,qGAAA,IAAI,OAAA;AAAE,6GAAA,YAAY,OAAA;AAAE,6GAAA,YAAY,OAAA;AAEzC,8DAAmE;AAA1D,uGAAA,KAAK,OAAA;AAAE,+GAAA,aAAa,OAAA;AAE7B,cAAc;AACd,8DAA4D;AAAnD,yGAAA,UAAU,OAAA;AASnB,mBAAmB;AACnB,sEAA0H;AAAjH,iHAAA,cAAc,OAAA;AAAE,kHAAA,eAAe,OAAA;AAAE,gHAAA,aAAa,OAAA;AAAE,uHAAA,oBAAoB,OAAA;AAE7E,eAAe;AACf,gDAA+C;AAAtC,sGAAA,SAAS,OAAA;AAElB,0BAA0B;AAC1B,mCAA2C;AAAlC,mGAAA,YAAY,OAAA;AACrB,6CAA0D;AAAjD,wGAAA,YAAY,OAAA;AAAE,oGAAA,QAAQ,OAAA;AAC/B,+CAAwE;AAA/D,sGAAA,SAAS,OAAA;AAAE,oGAAA,OAAO,OAAA;AAAE,4GAAA,eAAe,OAAA;AAG5C,kBAAkB;AAClB,yCACqG;AAD5F,oGAAA,UAAU,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,uGAAA,aAAa,OAAA;AAAE,qGAAA,WAAW,OAAA;AAC3D,gHAAA,sBAAsB,OAAA;AAAE,+GAAA,qBAAqB,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAAE,+FAAA,KAAK,OAAA;AAC5E,uDAAoD;AAA3C,4GAAA,WAAW,OAAA;AAEpB,4BAA4B;AAC5B,+CAA0G;AAAjG,+GAAA,kBAAkB,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,6GAAA,gBAAgB,OAAA;AAAE,2GAAA,cAAc,OAAA;AAG9E,mBAAmB;AACnB,sDACqF;AAD5E,oHAAA,sBAAsB,OAAA;AAAE,kHAAA,oBAAoB,OAAA;AAAE,4GAAA,cAAc,OAAA;AACnE,oHAAA,sBAAsB,OAAA;AAAE,uHAAA,yBAAyB,OAAA;AACnD,oDAAwD;AAA/C,2GAAA,cAAc,OAAA;AAEvB,oEAAoE;AACpE,wEAAwE;AACxE,8EAA8E;AAC9E,8EAA8E;AAC9E,+CAA+C;AAC/C,iDAG6D;AAHpD,oHAAA,sBAAsB,OAAA;AAAE,gHAAA,kBAAkB,OAAA;AAAE,kHAAA,oBAAoB,OAAA;AACvE,gHAAA,kBAAkB,OAAA;AAAE,oHAAA,sBAAsB,OAAA;AAC1C,4GAAA,cAAc,OAAA;AAAE,6GAAA,eAAe,OAAA;AAC/B,yGAAA,WAAW,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AAIhC,iBAAiB;AACjB,4CAAuD;AAA9C,4GAAA,kBAAkB,OAAA;AAE3B,8CAAgI;AAAvH,0GAAA,eAAe,OAAA;AAAE,6GAAA,kBAAkB,OAAA;AAAE,2GAAA,gBAAgB,OAAA;AAAE,6GAAA,kBAAkB,OAAA;AAAE,0GAAA,eAAe,OAAA;AAEnG,2BAA2B;AAC3B,8DAAgF;AAAvE,oHAAA,iBAAiB,OAAA;AAAE,kHAAA,eAAe,OAAA;AAC3C,0EAA2H;AAAlH,oIAAA,2BAA2B,OAAA;AAAE,yHAAA,gBAAgB,OAAA;AAAE,iIAAA,wBAAwB,OAAA;AAGhF,qBAAqB;AACrB,kDACoF;AAD3E,wGAAA,aAAa,OAAA;AAAE,0GAAA,eAAe,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,yGAAA,cAAc,OAAA;AACpE,gHAAA,qBAAqB,OAAA;AAAE,mGAAA,QAAQ,OAAA;AAAE,2GAAA,gBAAgB,OAAA;AAEnD,gEAA2F;AAAlF,oHAAA,kBAAkB,OAAA;AAAE,wHAAA,sBAAsB,OAAA;AAEnD,gBAAgB;AAChB,6DAA4D;AAAnD,kHAAA,eAAe,OAAA;AAExB,qDAA4E;AAAnE,kHAAA,mBAAmB,OAAA;AAAE,6GAAA,cAAc,OAAA;AAC5C,yCAA+C;AAAtC,qGAAA,YAAY,OAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@90soft/parse-server-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.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",
|