@zuzjs/flare-admin 0.1.4 → 0.1.6
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/README.md +312 -138
- package/dist/index.cjs +4 -4
- package/dist/index.d.cts +578 -47
- package/dist/index.d.ts +94 -11
- package/dist/index.js +3 -3
- package/dist/lib/grpc.d.ts +11 -0
- package/dist/lib/http.d.ts +3 -0
- package/dist/lib/notifications.d.ts +1 -1
- package/dist/lib/storage.d.ts +98 -0
- package/dist/realtime/Connection.d.ts +14 -0
- package/dist/realtime/LiveCollection.d.ts +3 -1
- package/dist/realtime/WsConnection.d.ts +11 -1
- package/dist/serverTimestamp.d.ts +13 -0
- package/dist/types/index.d.ts +357 -0
- package/package.json +4 -1
- package/proto/admin.proto +129 -0
- package/proto/app.proto +69 -0
- package/proto/auth.proto +70 -0
- package/proto/flare.proto +11 -0
- package/proto/query.proto +109 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ admin.auth()
|
|
|
22
22
|
socket elevated ✓
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
**No MongoDB URI is ever shared.** The SDK talks to FlareServer's `/admin/token` REST endpoint using only the `adminKey`.
|
|
25
|
+
**No MongoDB URI is ever shared.** The SDK talks to FlareServer's `/admin/token` REST endpoint using only the `adminKey`.
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -44,8 +44,6 @@ pnpm add @zuzjs/flare-admin
|
|
|
44
44
|
flare app create my-app
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
This prints two configs:
|
|
48
|
-
|
|
49
47
|
| Config | Safe for... |
|
|
50
48
|
|---|---|
|
|
51
49
|
| `apiKey` | Browser / client-side |
|
|
@@ -55,7 +53,7 @@ This prints two configs:
|
|
|
55
53
|
|
|
56
54
|
```bash
|
|
57
55
|
# .env (server-side only)
|
|
58
|
-
FLARE_URL=
|
|
56
|
+
FLARE_URL=https://flare.zuzcdn.net
|
|
59
57
|
FLARE_APP_ID=my-app
|
|
60
58
|
FLARE_ADMIN_KEY=FA_ADMIN_xxxxxxxxxxxx
|
|
61
59
|
```
|
|
@@ -78,14 +76,9 @@ const admin = connectApp({
|
|
|
78
76
|
import { getApp } from "@zuzjs/flare-admin";
|
|
79
77
|
|
|
80
78
|
app.post("/login", async (req, res) => {
|
|
81
|
-
// --- your existing auth logic ---
|
|
82
79
|
const user = await myAuthLogic(req.body);
|
|
83
80
|
if (!user) return res.status(401).json({ error: "invalid credentials" });
|
|
84
81
|
|
|
85
|
-
// Set your own session / cookie as normal
|
|
86
|
-
req.session.userId = user.id;
|
|
87
|
-
|
|
88
|
-
// --- the bridge ---
|
|
89
82
|
const flareToken = await getApp().auth().createCustomToken(user.id, {
|
|
90
83
|
role: user.isAdmin ? "admin" : "user",
|
|
91
84
|
claims: { email: user.email, plan: user.plan },
|
|
@@ -104,12 +97,11 @@ import FlareClient from "@zuzjs/flare";
|
|
|
104
97
|
const flare = new FlareClient({
|
|
105
98
|
endpoint: "https://flare.zuzcdn.net",
|
|
106
99
|
appId: "my-app",
|
|
107
|
-
apiKey: "FA_xxxxxxxx",
|
|
100
|
+
apiKey: "FA_xxxxxxxx",
|
|
108
101
|
});
|
|
109
102
|
|
|
110
103
|
flare.connect();
|
|
111
104
|
|
|
112
|
-
// After login:
|
|
113
105
|
const { flareToken } = await fetch("/login", { method: "POST", body: ... }).then(r => r.json());
|
|
114
106
|
await flare.auth(flareToken);
|
|
115
107
|
// ✅ Socket is now elevated — auth.uid and auth.role are set
|
|
@@ -125,51 +117,80 @@ Initialize a FlareAdmin app. Idempotent — safe to call at module scope.
|
|
|
125
117
|
|
|
126
118
|
```typescript
|
|
127
119
|
const admin = connectApp({
|
|
128
|
-
serverUrl:
|
|
129
|
-
appId:
|
|
130
|
-
adminKey:
|
|
131
|
-
|
|
120
|
+
serverUrl: string; // FlareServer base URL
|
|
121
|
+
appId: string; // App ID from `flare app create`
|
|
122
|
+
adminKey: string; // Admin key — server-side only
|
|
123
|
+
httpBase?: string; // Override base URL for all admin HTTP APIs (e.g. proxy)
|
|
124
|
+
defaultTtl?: string; // Default token TTL, e.g. "24h" (default)
|
|
125
|
+
dataMapper?: Record<string, (row: any) => any>; // Per-collection response mappers
|
|
132
126
|
});
|
|
133
127
|
```
|
|
134
128
|
|
|
129
|
+
`httpBase` routes all admin HTTP traffic through a different base URL (useful for proxies or gateways). When set, it replaces `serverUrl` as the base for every `/admin/*` HTTP request. Trailing slashes are normalized automatically.
|
|
130
|
+
|
|
135
131
|
### `getApp(name?)`
|
|
136
132
|
|
|
137
|
-
Retrieve an already-initialized app instance.
|
|
133
|
+
Retrieve an already-initialized app instance. Throws if not initialized.
|
|
138
134
|
|
|
139
|
-
### `
|
|
135
|
+
### `disconnectApp(name?)`
|
|
140
136
|
|
|
141
|
-
|
|
137
|
+
Disconnect and remove an app from the registry. Returns `true` if the app existed.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
disconnectApp(); // default app
|
|
141
|
+
disconnectApp("app-a"); // named app
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `disconnectAllApps()`
|
|
145
|
+
|
|
146
|
+
Disconnect and clear every initialized app.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
disconnectAllApps();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `app.disconnect()`
|
|
153
|
+
|
|
154
|
+
Close the WebSocket connection on a specific app instance. Safe to call multiple times.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
admin.disconnect();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Auth
|
|
142
163
|
|
|
143
164
|
### `admin.auth().createCustomToken(uid, opts?)`
|
|
144
165
|
|
|
145
|
-
Mint a custom auth token.
|
|
166
|
+
Mint a custom auth token for use by the browser client.
|
|
146
167
|
|
|
147
168
|
```typescript
|
|
148
169
|
const token = await admin.auth().createCustomToken(uid, {
|
|
149
170
|
role?: "user" | "admin" | "anon", // default: "user"
|
|
150
|
-
claims?: Record<string, unknown>,
|
|
171
|
+
claims?: Record<string, unknown>,
|
|
151
172
|
ttl?: string, // e.g. "1h", "7d"
|
|
152
173
|
});
|
|
153
174
|
```
|
|
154
175
|
|
|
155
176
|
### `admin.auth().getTicket(uid, opts?)`
|
|
156
177
|
|
|
157
|
-
Mint a one-time ticket
|
|
178
|
+
Mint a one-time ticket for WebSocket auth flows.
|
|
158
179
|
|
|
159
180
|
```typescript
|
|
160
181
|
const ticket = await admin.auth().getTicket(uid, {
|
|
161
|
-
role?:
|
|
162
|
-
email?:
|
|
163
|
-
sid?:
|
|
164
|
-
ttlSeconds?: number,
|
|
165
|
-
ip?:
|
|
182
|
+
role?: "user" | "admin" | "anon",
|
|
183
|
+
email?: string,
|
|
184
|
+
sid?: string,
|
|
185
|
+
ttlSeconds?: number,
|
|
186
|
+
ip?: string,
|
|
166
187
|
});
|
|
167
188
|
|
|
168
|
-
// ticket shape
|
|
189
|
+
// ticket shape:
|
|
169
190
|
// {
|
|
170
|
-
// ticket: "websocket:550e8400
|
|
191
|
+
// ticket: "websocket:550e8400-...",
|
|
171
192
|
// tag: "websocket",
|
|
172
|
-
// uuid: "550e8400
|
|
193
|
+
// uuid: "550e8400-...",
|
|
173
194
|
// expires_at: "2026-04-15T12:34:56Z",
|
|
174
195
|
// one_time: true,
|
|
175
196
|
// uid: "user_123",
|
|
@@ -178,39 +199,157 @@ const ticket = await admin.auth().getTicket(uid, {
|
|
|
178
199
|
// }
|
|
179
200
|
```
|
|
180
201
|
|
|
181
|
-
|
|
202
|
+
---
|
|
182
203
|
|
|
183
|
-
|
|
184
|
-
POST /admin/ticket
|
|
185
|
-
Authorization: Bearer FA_ADMIN_xxxxxxxxxxxx
|
|
186
|
-
Content-Type: application/json
|
|
204
|
+
## Database
|
|
187
205
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
206
|
+
```typescript
|
|
207
|
+
// One-shot queries (bypasses security rules)
|
|
208
|
+
const users = await admin.db().collection("users").get();
|
|
209
|
+
|
|
210
|
+
await admin.db().collection("users").doc("alice").set({ name: "Alice" });
|
|
211
|
+
await admin.db().collection("users").doc("alice").update({ plan: "pro" });
|
|
212
|
+
await admin.db().collection("users").doc("alice").delete();
|
|
213
|
+
|
|
214
|
+
// Rich queries
|
|
215
|
+
const seniors = await admin.db()
|
|
216
|
+
.collection("users")
|
|
217
|
+
.where({ age: ">= 60" })
|
|
218
|
+
.orderBy("name")
|
|
219
|
+
.limit(10)
|
|
220
|
+
.get();
|
|
194
221
|
```
|
|
195
222
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
### Query Builder
|
|
224
|
+
|
|
225
|
+
`admin.db().collection(...)` supports the full structured query API:
|
|
226
|
+
|
|
227
|
+
**Filters:** `where`, `and`, `or`, `in`, `andIn`, `orIn`, `notIn`, `andNotIn`, `orNotIn`, `arrayContains`, `andArrayContains`, `orArrayContains`, `arrayContainsAny`, `andArrayContainsAny`, `orArrayContainsAny`, `some`, `andSome`, `orSome`, `like`, `andLike`, `orLike`, `notLike`, `andNotLike`, `orNotLike`, `exists`, `andExists`, `orExists`, `notExists`, `andNotExists`, `orNotExists`
|
|
228
|
+
|
|
229
|
+
**Sort / cursor / aggregate:** `latest`, `newest`, `oldest`, `orderBy`, `limit`, `offset`, `startAt`, `startAfter`, `endAt`, `endBefore`, `count`, `sum`, `avg`, `min`, `max`, `distinct`, `groupBy`, `having`, `select`, `distinctField`, `vectorSearch`
|
|
230
|
+
|
|
231
|
+
**Joins:** `join`, `Join`, `joinNested`, `JoinNested`, `withRelation`
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const rows = await admin.db()
|
|
235
|
+
.collection("boards")
|
|
236
|
+
.where({ uid: userId })
|
|
237
|
+
.orSome("team", { uid: userId })
|
|
238
|
+
.join("lists", { source: "id", target: "boardId", as: "lists" })
|
|
239
|
+
.join("users", { source: "team.uid", target: "id", as: "teamMembers" })
|
|
240
|
+
.joinNested("lists", "cards", { source: "id", target: "listId", as: "cards" })
|
|
241
|
+
.withRelation("team.uid->users.id as collaborators")
|
|
242
|
+
.orderBy("updatedAt", "desc")
|
|
243
|
+
.limit(20)
|
|
244
|
+
.get();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `allowSensitiveAuthUserFields(false)`
|
|
248
|
+
|
|
249
|
+
Auth-user joins default to full fields. Pass `false` to restrict to public-profile-only output:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const safeBoards = await admin.db()
|
|
253
|
+
.collection("boards")
|
|
254
|
+
.join("users", { source: "team.uid", target: "id", as: "team" })
|
|
255
|
+
.allowSensitiveAuthUserFields(false)
|
|
256
|
+
.get();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### `getRawQuery()`
|
|
260
|
+
|
|
261
|
+
Inspect the structured query that will be sent:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const raw = admin.db()
|
|
265
|
+
.collection("boards")
|
|
266
|
+
.where({ uid: userId })
|
|
267
|
+
.getRawQuery();
|
|
268
|
+
|
|
269
|
+
console.log(raw.collection, raw.query);
|
|
209
270
|
```
|
|
210
271
|
|
|
211
272
|
---
|
|
212
273
|
|
|
213
|
-
##
|
|
274
|
+
## Realtime
|
|
275
|
+
|
|
276
|
+
### `admin.live().collection(...).onSnapshot(cb)`
|
|
277
|
+
|
|
278
|
+
Single-snapshot subscription — fires once on the initial data load, then auto-unsubscribes:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const unsub = admin.live()
|
|
282
|
+
.collection("orders")
|
|
283
|
+
.where({ status: "pending" })
|
|
284
|
+
.orderBy("createdAt", "desc")
|
|
285
|
+
.onSnapshot((snap) => {
|
|
286
|
+
console.log(snap.type, snap.data);
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `.stream(options?)` — live batched stream
|
|
291
|
+
|
|
292
|
+
Returns a long-lived `AdminCollectionStream<T>` that maintains a local snapshot and fans out batched change events to listeners.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
const stream = admin.live()
|
|
296
|
+
.collection("orders")
|
|
297
|
+
.where({ status: "pending" })
|
|
298
|
+
.stream({
|
|
299
|
+
flushMs?: number, // batch flush delay in ms (default: 24)
|
|
300
|
+
maxBatchSize?: number, // max changes per flush (default: 200)
|
|
301
|
+
insertAt?: "start" | "end", // where new docs land (default: "end")
|
|
302
|
+
maxDocs?: number, // cap the local snapshot size
|
|
303
|
+
sort?: (a: T, b: T) => number, // comparator applied after each flush
|
|
304
|
+
idField?: string, // identity field name (default: "id")
|
|
305
|
+
getId?: (doc: T) => string, // custom id extractor
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Subscribe to changes
|
|
309
|
+
const off = stream.listen((docs, meta) => {
|
|
310
|
+
console.log(meta.reason, meta.version, docs.length);
|
|
311
|
+
// meta.reason: "snapshot" | "change-batch"
|
|
312
|
+
// meta.ready: true after the first snapshot arrives
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Read the current snapshot without subscribing
|
|
316
|
+
const current = stream.getSnapshot();
|
|
317
|
+
|
|
318
|
+
// Remove a specific listener (does not close the stream)
|
|
319
|
+
off();
|
|
320
|
+
|
|
321
|
+
// Close the stream and release the WebSocket subscription
|
|
322
|
+
stream.close();
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### `.asStore(options?)` — framework-agnostic external store
|
|
326
|
+
|
|
327
|
+
Returns an `AdminCollectionExternalStore<T>` compatible with React `useSyncExternalStore` or any subscribe/getSnapshot pattern:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
const store = admin.live()
|
|
331
|
+
.collection("orders")
|
|
332
|
+
.asStore({ flushMs: 50 });
|
|
333
|
+
|
|
334
|
+
// React
|
|
335
|
+
import { useSyncExternalStore } from "react";
|
|
336
|
+
const orders = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
337
|
+
|
|
338
|
+
// Or directly
|
|
339
|
+
const unsub = store.subscribe(() => {
|
|
340
|
+
console.log(store.getSnapshot());
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
store.close(); // release when done
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Realtime query builder parity
|
|
347
|
+
|
|
348
|
+
The same query builder surface (`where`, `orderBy`, `join`, `limit`, etc.) is available on `admin.live().collection(...)` just like on `admin.db().collection(...)`.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Security rules
|
|
214
353
|
|
|
215
354
|
Once a client calls `flare.auth(token)`, the FlareServer socket has:
|
|
216
355
|
|
|
@@ -219,8 +358,6 @@ auth.uid = the uid you passed to createCustomToken()
|
|
|
219
358
|
auth.role = "user" | "admin" | "anon"
|
|
220
359
|
```
|
|
221
360
|
|
|
222
|
-
Use these in your security rules:
|
|
223
|
-
|
|
224
361
|
```json
|
|
225
362
|
{
|
|
226
363
|
"users": { ".read": "auth != null", ".write": "auth.uid == $docId" },
|
|
@@ -240,129 +377,166 @@ const adminB = connectApp({ serverUrl, appId: "app-b", adminKey: "..." }, "b");
|
|
|
240
377
|
|
|
241
378
|
const tokenA = await getApp("a").auth().createCustomToken(userId);
|
|
242
379
|
const tokenB = await getApp("b").auth().createCustomToken(userId);
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
## Admin-Only Auth Join Field Control
|
|
246
380
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Use `allowSensitiveAuthUserFields(false)` per query when you want public-profile-only output:
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
const safeBoards = await admin.db()
|
|
253
|
-
.collection("boards")
|
|
254
|
-
.join("users", { source: "team.uid", target: "id", as: "team" })
|
|
255
|
-
.allowSensitiveAuthUserFields(false)
|
|
256
|
-
.get();
|
|
381
|
+
disconnectAllApps();
|
|
257
382
|
```
|
|
258
383
|
|
|
259
|
-
|
|
260
|
-
- This option is admin-only.
|
|
261
|
-
- Normal client/system query paths cannot enable sensitive auth-user fields.
|
|
262
|
-
|
|
263
|
-
## Data Mapper (Admin)
|
|
384
|
+
---
|
|
264
385
|
|
|
265
|
-
|
|
386
|
+
## Data Mapper
|
|
266
387
|
|
|
267
|
-
|
|
268
|
-
- Base collection rows use the mapper key matching the collection name.
|
|
269
|
-
- Join payload rows use the mapper key matching join `as`.
|
|
388
|
+
Pass `dataMapper` in `connectApp(...)` to shape inbound data. Keys match collection names or join aliases (`as`).
|
|
270
389
|
|
|
271
390
|
```typescript
|
|
272
391
|
const admin = connectApp({
|
|
273
392
|
serverUrl: process.env.FLARE_URL!,
|
|
274
|
-
appId:
|
|
275
|
-
adminKey:
|
|
393
|
+
appId: process.env.FLARE_APP_ID!,
|
|
394
|
+
adminKey: process.env.FLARE_ADMIN_KEY!,
|
|
276
395
|
dataMapper: {
|
|
277
396
|
boards: (row) => ({
|
|
278
|
-
id:
|
|
279
|
-
name:
|
|
280
|
-
description: row.description,
|
|
397
|
+
id: row.id,
|
|
398
|
+
name: row.name,
|
|
281
399
|
createdAt: new Date(row.createdAt ?? row.created_at),
|
|
282
400
|
}),
|
|
283
401
|
team: (row) => ({
|
|
284
|
-
id:
|
|
285
|
-
name:
|
|
402
|
+
id: row.id,
|
|
403
|
+
name: row.authMeta?.additionalParams?.name || "Unknown",
|
|
286
404
|
email: row.email,
|
|
287
|
-
createdAt: new Date(row.createdAt ?? row.created_at),
|
|
288
405
|
}),
|
|
289
406
|
},
|
|
290
407
|
});
|
|
291
|
-
|
|
292
|
-
const rows = await admin.db()
|
|
293
|
-
.collection("boards")
|
|
294
|
-
.where({ boardId: "123" })
|
|
295
|
-
.join("users", {
|
|
296
|
-
source: "team.uid",
|
|
297
|
-
target: "uid",
|
|
298
|
-
as: "team",
|
|
299
|
-
})
|
|
300
|
-
.get();
|
|
301
|
-
|
|
302
|
-
// rows[*] is mapped by dataMapper.boards
|
|
303
|
-
// rows[*].team[*] is mapped by dataMapper.team (join alias)
|
|
304
408
|
```
|
|
305
409
|
|
|
306
|
-
|
|
410
|
+
For `join(..., { as: "team" })`, define `dataMapper.team`.
|
|
307
411
|
|
|
308
412
|
---
|
|
309
413
|
|
|
310
|
-
##
|
|
414
|
+
## Storage API (S3-like)
|
|
311
415
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
### Supported filter helpers
|
|
416
|
+
```typescript
|
|
417
|
+
import { connectApp, AdminStorageSignedAction } from "@zuzjs/flare-admin";
|
|
315
418
|
|
|
316
|
-
|
|
419
|
+
const storage = admin.storage();
|
|
317
420
|
|
|
318
|
-
|
|
421
|
+
await storage.createBucket("reports");
|
|
319
422
|
|
|
320
|
-
|
|
423
|
+
await storage.putObject({
|
|
424
|
+
bucket: "reports",
|
|
425
|
+
key: "weekly/summary.json",
|
|
426
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
427
|
+
contentType: "application/json",
|
|
428
|
+
encrypt: true,
|
|
429
|
+
});
|
|
321
430
|
|
|
322
|
-
|
|
431
|
+
const meta = await storage.headObject({ bucket: "reports", key: "weekly/summary.json" });
|
|
432
|
+
const downloaded = await storage.getObject({ bucket: "reports", key: "weekly/summary.json", decrypt: true });
|
|
323
433
|
|
|
324
|
-
|
|
434
|
+
await storage.deleteObjects({ bucket: "reports", keys: ["weekly/summary.json"] });
|
|
435
|
+
```
|
|
325
436
|
|
|
326
|
-
|
|
437
|
+
### Signed URLs
|
|
327
438
|
|
|
328
439
|
```typescript
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
.
|
|
332
|
-
.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
.
|
|
440
|
+
const signedUpload = await admin.createSignedUrl({
|
|
441
|
+
bucket: "reports",
|
|
442
|
+
key: "uploads/big-video.mp4",
|
|
443
|
+
action: AdminStorageSignedAction.Upload,
|
|
444
|
+
expiresInSeconds: 300,
|
|
445
|
+
contentType: "video/mp4",
|
|
446
|
+
encrypt: true,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
await fetch(signedUpload.url, {
|
|
450
|
+
method: signedUpload.method,
|
|
451
|
+
headers: { "Content-Type": "video/mp4" },
|
|
452
|
+
body: videoBuffer,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const signedDownload = await admin.createSignedUrl({
|
|
456
|
+
bucket: "reports",
|
|
457
|
+
key: "uploads/big-video.mp4",
|
|
458
|
+
action: AdminStorageSignedAction.Download,
|
|
459
|
+
expiresInSeconds: 300,
|
|
460
|
+
decrypt: true,
|
|
461
|
+
allowedOrigins: ["https://app.example.com"],
|
|
462
|
+
});
|
|
340
463
|
```
|
|
341
464
|
|
|
342
|
-
|
|
465
|
+
Notes:
|
|
466
|
+
- `forceDownload` and `embedOnly` are mutually exclusive.
|
|
467
|
+
- `allowedOrigins` defaults to `['*']` if omitted.
|
|
343
468
|
|
|
344
|
-
|
|
469
|
+
### Direct download helpers
|
|
345
470
|
|
|
346
471
|
```typescript
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
472
|
+
const directUrl = await admin.getObjectUrl({
|
|
473
|
+
bucket: "reports",
|
|
474
|
+
key: "weekly/summary.json",
|
|
475
|
+
expiresInSeconds: 120,
|
|
476
|
+
allowedOrigins: ["*"],
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const triggered = await admin.downloadObject({
|
|
480
|
+
bucket: "reports",
|
|
481
|
+
key: "weekly/summary.json",
|
|
482
|
+
filename: "summary.json",
|
|
483
|
+
forceDownload: true,
|
|
484
|
+
allowedOrigins: ["https://app.example.com"],
|
|
485
|
+
});
|
|
354
486
|
```
|
|
355
487
|
|
|
356
|
-
###
|
|
488
|
+
### Storage rules
|
|
489
|
+
|
|
490
|
+
```txt
|
|
491
|
+
service zuz.storage {
|
|
492
|
+
match /storage/{bucket}/objects {
|
|
493
|
+
match /{document=**} {
|
|
494
|
+
allow read: if auth != null;
|
|
495
|
+
|
|
496
|
+
allow write: if auth != null
|
|
497
|
+
&& requestData.storage.usage.storageBytes + requestData.size
|
|
498
|
+
<= requestData.storage.plan.storageBytes;
|
|
499
|
+
|
|
500
|
+
allow delete: if auth != null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
357
505
|
|
|
358
|
-
|
|
506
|
+
### External AWS SDK from Flare `/aws` config
|
|
359
507
|
|
|
360
508
|
```typescript
|
|
361
|
-
|
|
362
|
-
.collection("boards")
|
|
363
|
-
.where({ uid: userId })
|
|
364
|
-
.withRelation("team.uid->users.id as collaborators")
|
|
365
|
-
.getRawQuery();
|
|
509
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
366
510
|
|
|
367
|
-
|
|
511
|
+
const aws = await admin.storage().awsConfig("storage-server-id");
|
|
512
|
+
|
|
513
|
+
const s3 = new S3Client({
|
|
514
|
+
endpoint: aws.endpoint,
|
|
515
|
+
region: aws.region,
|
|
516
|
+
forcePathStyle: Boolean(aws.forcePathStyle),
|
|
517
|
+
credentials: {
|
|
518
|
+
accessKeyId: aws.accessKeyId,
|
|
519
|
+
secretAccessKey: aws.secretAccessKey,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await s3.send(new PutObjectCommand({
|
|
524
|
+
Bucket: aws.bucket,
|
|
525
|
+
Key: `${aws.prefix ?? ""}manual/test.txt`,
|
|
526
|
+
Body: "hello",
|
|
527
|
+
ContentType: "text/plain",
|
|
528
|
+
}));
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Push Notifications
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
await admin.notifications().send({
|
|
537
|
+
uid: "user_123",
|
|
538
|
+
title: "Hello",
|
|
539
|
+
body: "Your order shipped!",
|
|
540
|
+
data: { orderId: "abc" },
|
|
541
|
+
});
|
|
368
542
|
```
|