@squidcloud/cli 1.0.487 → 1.0.489
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/dist/index.js +69 -19
- package/dist/resources/claude/skills/squid-development/SKILL.md +8 -8
- package/dist/resources/claude/skills/squid-development/reference/admin.md +30 -0
- package/dist/resources/claude/skills/squid-development/reference/ai.md +372 -10
- package/dist/resources/claude/skills/squid-development/reference/api.md +46 -1
- package/dist/resources/claude/skills/squid-development/reference/backend.md +57 -1
- package/dist/resources/claude/skills/squid-development/reference/chat-widget.md +1 -1
- package/dist/resources/claude/skills/squid-development/reference/client.md +75 -4
- package/dist/resources/claude/skills/squid-development/reference/console.md +6 -1
- package/dist/resources/claude/skills/squid-development/reference/databases.md +48 -0
- package/dist/resources/claude/skills/squid-development/reference/openai.md +1 -1
- package/dist/resources/claude/skills/squid-development/reference/security.md +74 -1
- package/dist/resources/claude/skills/squid-integrations/reference/connector-functions.md +1 -1
- package/dist/resources/claude/skills/squid-react-development/SKILL.md +21 -4
- package/package.json +2 -2
|
@@ -18,6 +18,7 @@ Docs: https://docs.getsquid.ai/reference-docs/backend/
|
|
|
18
18
|
- Schedulers (@scheduler)
|
|
19
19
|
- Rate Limiting (@limits)
|
|
20
20
|
- Queue Message Handlers (@onQueueMessage)
|
|
21
|
+
- Events (@eventHandler)
|
|
21
22
|
- Client Connection State (@clientConnectionStateHandler)
|
|
22
23
|
- Cross-Service Communication (squidInject)
|
|
23
24
|
- Using Squid Client in Backend
|
|
@@ -37,8 +38,10 @@ squid init backend --appId YOUR_APP_ID --apiKey YOUR_API_KEY --environmentId dev
|
|
|
37
38
|
|
|
38
39
|
**`squid start`** - Runs backend locally with hot-reload. Connects to Squid Cloud via reverse proxy.
|
|
39
40
|
```bash
|
|
40
|
-
cd backend && squid start
|
|
41
|
+
cd backend && squid start [--printBundle]
|
|
41
42
|
```
|
|
43
|
+
`--printBundle` makes the local dev server log the full application bundle data on startup instead of
|
|
44
|
+
just the webhooks — useful when checking what the build actually registered.
|
|
42
45
|
|
|
43
46
|
**`squid deploy`** - Builds and deploys to Squid Cloud.
|
|
44
47
|
```bash
|
|
@@ -50,6 +53,20 @@ squid deploy [--apiKey KEY] [--environmentId prod] [--skipBuild]
|
|
|
50
53
|
squid build [--dev] [--skip-version-check]
|
|
51
54
|
```
|
|
52
55
|
|
|
56
|
+
**`squid kb-upload`** - Bulk-ingests a local directory into a knowledge base using the durable bulk
|
|
57
|
+
ingestion pipeline (presigned uploads + provider batch APIs). Walks `--dir` recursively, stages files
|
|
58
|
+
in batches, and polls each job to completion.
|
|
59
|
+
```bash
|
|
60
|
+
squid kb-upload --dir ./docs --knowledgeBase my-kb
|
|
61
|
+
```
|
|
62
|
+
Options: `--dir` (required), `--knowledgeBase` (required), `--appId`/`--apiKey`/`--region`/`--environmentId`
|
|
63
|
+
(fall back to `SQUID_APP_ID`, `SQUID_API_KEY`, `SQUID_REGION`, `SQUID_ENVIRONMENT_ID`),
|
|
64
|
+
`--batchSize` (files per job, default 200, max 1000), `--extensions` (comma-separated allow-list;
|
|
65
|
+
defaults to pdf, docx, txt, md, html, csv, xlsx, xls, xlsm, xlsb, pptx), `--dryRun` (list files and
|
|
66
|
+
exit without contacting the server), `--timeoutMinutes` (per-job wait budget, default 120 — on
|
|
67
|
+
timeout the CLI reports the job as still running server-side and moves on).
|
|
68
|
+
Ctrl-C cancels the in-flight job. See [ai.md](ai.md#bulk-ingestion) for the SDK equivalent.
|
|
69
|
+
|
|
53
70
|
**Extended logging** - Add to `.env`:
|
|
54
71
|
```env
|
|
55
72
|
SQUID_LOG_TYPES=QUERY,MUTATION,AI,API,ERROR
|
|
@@ -500,6 +517,33 @@ export class MyService extends SquidService {
|
|
|
500
517
|
- To secure the topic, use `@secureTopic` — see [security.md](security.md)
|
|
501
518
|
- The handler receives each message individually (not batched)
|
|
502
519
|
|
|
520
|
+
## Events (@eventHandler)
|
|
521
|
+
|
|
522
|
+
`@eventHandler(type)` subscribes a backend function to events emitted with
|
|
523
|
+
`squid.events().emit(...)` (see [client.md](client.md#events)). Unlike `@onQueueMessage`, **every**
|
|
524
|
+
handler registered for the type receives the event, so one event can fan out to several services.
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { SquidService, eventHandler } from '@squidcloud/backend';
|
|
528
|
+
import { TriggerEvent } from '@squidcloud/client';
|
|
529
|
+
|
|
530
|
+
export class OrderEventsService extends SquidService {
|
|
531
|
+
@eventHandler<{ orderId: string; total: number }>('order.created')
|
|
532
|
+
async onOrderCreated(event: TriggerEvent<{ orderId: string; total: number }>): Promise<void> {
|
|
533
|
+
console.log(event.id); // unique event id
|
|
534
|
+
console.log(event.type); // 'order.created'
|
|
535
|
+
console.log(event.payload.orderId);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Notes:**
|
|
541
|
+
- Delivery is at-least-once with **no ordering guarantee** — handlers must be idempotent and
|
|
542
|
+
order-independent.
|
|
543
|
+
- Emitting requires API key authentication; a user token is rejected.
|
|
544
|
+
- Multiple subscribers per type are supported (that is the point — use `@onQueueMessage` when you
|
|
545
|
+
want queue semantics instead).
|
|
546
|
+
|
|
503
547
|
## Client Connection State (@clientConnectionStateHandler)
|
|
504
548
|
|
|
505
549
|
Handle client connection and disconnection events.
|
|
@@ -620,6 +664,18 @@ export class MyService extends SquidService {
|
|
|
620
664
|
}
|
|
621
665
|
```
|
|
622
666
|
|
|
667
|
+
**`getSquid()` / `getPassiveSquid()`** — `getSquid()` returns the same instance backing `this.squid`.
|
|
668
|
+
`getPassiveSquid()` returns a dedicated **passive-mode** instance (HTTP-only, no WebSocket), kept
|
|
669
|
+
separate from the main one. Use it for one-off HTTP calls that should not open or keep a socket.
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
import { getSquid, getPassiveSquid } from '@squidcloud/backend';
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**Experimental:** `this.workspace` (a `WorkspaceClient`) gives a backend-only scratch directory
|
|
676
|
+
synced across pod replicas via the shared workspace server — distinct from the tenant's shared
|
|
677
|
+
storage.
|
|
678
|
+
|
|
623
679
|
## File Handling
|
|
624
680
|
|
|
625
681
|
```typescript
|
|
@@ -763,7 +763,7 @@ declare namespace JSX {
|
|
|
763
763
|
squid-ai-instructions="Be concise and professional."
|
|
764
764
|
squid-ai-functions="getOrderStatus,lookupAccount"
|
|
765
765
|
squid-ai-connected-agents='[{"agentId":"billing-agent","description":"Handles billing questions"}]'
|
|
766
|
-
squid-ai-override-model="gpt-5-
|
|
766
|
+
squid-ai-override-model="gpt-5.6-terra"
|
|
767
767
|
squid-ai-temperature="0.7"
|
|
768
768
|
squid-ai-max-tokens="2000"
|
|
769
769
|
squid-ai-agent-chat-options='{
|
|
@@ -18,6 +18,8 @@ Docs: https://docs.getsquid.ai/reference-docs/typescript-client/
|
|
|
18
18
|
- Web
|
|
19
19
|
- Extraction
|
|
20
20
|
- Jobs
|
|
21
|
+
- Events
|
|
22
|
+
- LangGraph
|
|
21
23
|
- Observability & Metrics
|
|
22
24
|
- Custom Notifications
|
|
23
25
|
|
|
@@ -74,8 +76,8 @@ Every Squid application includes the **Essentials Connector** - a built-in integ
|
|
|
74
76
|
|---------|--------|-------------|------|
|
|
75
77
|
| **Web Utilities** | `squid.web()` | AI-powered web search, URL content extraction, short URLs | [Web section](#web) |
|
|
76
78
|
| **AI Agents** | `squid.ai().agent()` | Chat with built-in or custom AI agents | [ai.md](ai.md) |
|
|
77
|
-
| **Knowledge Bases** | `squid.ai().knowledgeBase()` | RAG with semantic
|
|
78
|
-
| **Image Generation** | `squid.ai().image()` | Generate images with
|
|
79
|
+
| **Knowledge Bases** | `squid.ai().knowledgeBase()` | RAG with semantic, keyword, and knowledge-graph search plus reranking | [ai.md](ai.md) |
|
|
80
|
+
| **Image Generation** | `squid.ai().image()` | Generate images with `gpt-image-*`, Stable Diffusion, or Flux | [ai.md](ai.md) |
|
|
79
81
|
| **Audio** | `squid.ai().audio()` | Transcription and text-to-speech | [ai.md](ai.md) |
|
|
80
82
|
| **PDF/Extraction** | `squid.extraction()` | Create PDFs, extract data from documents | [Extraction section](#extraction) |
|
|
81
83
|
| **Observability** | `squid.observability` | Report and query custom metrics | [Observability section](#observability--metrics) |
|
|
@@ -324,9 +326,9 @@ const result = await squid.withLock('payment-processing', async (lock) => {
|
|
|
324
326
|
- `maxHoldTimeMillis` (number, default: no limit) - Max time in ms the lock can be held before automatic release. If not set, the lock is held until explicitly released or the connection is lost.
|
|
325
327
|
|
|
326
328
|
**DistributedLock methods:**
|
|
327
|
-
- `release()` - Release the lock
|
|
329
|
+
- `release()` - Release the lock. The lock is considered released immediately; the message to the server is sent asynchronously.
|
|
328
330
|
- `isReleased()` - Check if already released
|
|
329
|
-
- `observeRelease()` - Observable that emits when lock is released (including unexpected release due to connection loss)
|
|
331
|
+
- `observeRelease()` - Observable that emits when lock is released (including unexpected release due to connection loss). It emits as soon as the lock is considered released **locally**, without waiting for the server to confirm, and it replays — a subscriber that attaches after the release still learns about it.
|
|
330
332
|
- `resourceId` - The mutex name
|
|
331
333
|
- `lockId` - Unique lock instance ID
|
|
332
334
|
|
|
@@ -344,6 +346,12 @@ const web = squid.web();
|
|
|
344
346
|
// AI-powered web search
|
|
345
347
|
const results = await web.aiSearch('latest AI developments');
|
|
346
348
|
|
|
349
|
+
// aiSearch(query, abortSignal?, allowedDomains?)
|
|
350
|
+
// - abortSignal cancels the in-flight HTTP request, bounding a single call.
|
|
351
|
+
// - allowedDomains restricts the search (subdomains included); every cited URL comes from that list.
|
|
352
|
+
const controller = new AbortController();
|
|
353
|
+
const scoped = await web.aiSearch('pricing changes', controller.signal, ['getsquid.ai', 'docs.getsquid.ai']);
|
|
354
|
+
|
|
347
355
|
// Get URL content (as markdown)
|
|
348
356
|
const content = await web.getUrlContent('https://example.com/article');
|
|
349
357
|
|
|
@@ -416,6 +424,69 @@ if (job?.status === 'completed') {
|
|
|
416
424
|
const result = await jobClient.awaitJob<Result>('job-123');
|
|
417
425
|
```
|
|
418
426
|
|
|
427
|
+
### Driving a job from your own code
|
|
428
|
+
|
|
429
|
+
A job can also be created and resolved by the caller. Use this when a long operation runs outside
|
|
430
|
+
Squid and clients should still `awaitJob` on it. These three methods require an API key.
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
const jobId = crypto.randomUUID();
|
|
434
|
+
|
|
435
|
+
await jobClient.startJob(jobId); // mark the job as running
|
|
436
|
+
await jobClient.completeJob(jobId, { rows: 42 }); // resolve waiters with a result
|
|
437
|
+
await jobClient.failJob(jobId, 'Upstream timed out'); // reject waiters with an error
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Events
|
|
441
|
+
|
|
442
|
+
Emit a generic event that every backend function declared with `@eventHandler` for the same type
|
|
443
|
+
receives. Delivery is durable and at-least-once with **no ordering guarantee**, so handlers must be
|
|
444
|
+
idempotent and order-independent. `emit()` resolves once the event is enqueued, not once handlers ran.
|
|
445
|
+
|
|
446
|
+
Requires API key authentication — calls authenticated with a user token are rejected.
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
import { generateUUID } from '@squidcloud/client';
|
|
450
|
+
|
|
451
|
+
await squid.events().emit({
|
|
452
|
+
id: generateUUID(), // unique event id
|
|
453
|
+
type: 'order.created', // selects the @eventHandler subscribers
|
|
454
|
+
payload: { orderId: 'o-1', total: 99.5 },
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Handle it in the backend — see [backend.md](backend.md#events-eventhandler).
|
|
459
|
+
|
|
460
|
+
## LangGraph
|
|
461
|
+
|
|
462
|
+
Invoke Python LangGraph graphs defined in a Python backend. Get a reference with
|
|
463
|
+
`squid.langGraph(graphId)`.
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
const graph = squid.langGraph('support-graph');
|
|
467
|
+
|
|
468
|
+
// Run and wait for the result (new thread when threadId is omitted)
|
|
469
|
+
const result = await graph.invoke({ input: { question: 'Where is my order?' } });
|
|
470
|
+
// result: { ok, threadId, status: 'completed' | 'interrupted', state, next }
|
|
471
|
+
|
|
472
|
+
// Run without holding a promise — returns a jobId to await later
|
|
473
|
+
const jobId = await graph.invokeAsync({ threadId: 'thread-1', input: { question: '...' } });
|
|
474
|
+
const later = await squid.job().awaitJob(jobId);
|
|
475
|
+
|
|
476
|
+
// Resume a graph paused on interrupt()
|
|
477
|
+
const resumed = await graph.resume('thread-1', { approved: true });
|
|
478
|
+
const resumeJobId = await graph.resumeAsync('thread-1', { approved: true });
|
|
479
|
+
|
|
480
|
+
// Inspect and clean up thread state
|
|
481
|
+
const state = await graph.getState('thread-1'); // { ok, threadId, state, next }
|
|
482
|
+
await graph.deleteThread('thread-1'); // idempotent; next run starts fresh
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Graph execution caps at the tenant IPC timeout (currently 4 minutes) for every variant — the `Async`
|
|
486
|
+
methods only free the caller from holding the promise.
|
|
487
|
+
|
|
488
|
+
Secure invocations with the backend `@secureLangGraph` decorator — see [security.md](security.md#securelanggraph).
|
|
489
|
+
|
|
419
490
|
## Observability & Metrics
|
|
420
491
|
|
|
421
492
|
Report and query custom metrics for monitoring application performance.
|
|
@@ -48,6 +48,7 @@ The AI Studio provides visual tools for building and testing AI agents:
|
|
|
48
48
|
### Agent Configuration
|
|
49
49
|
- Create and configure AI agents visually
|
|
50
50
|
- Set agent instructions, models, and guardrails
|
|
51
|
+
- Refuse prompts containing PII under Guardrails → Prompt Privacy (toggle, a checkbox per kind, and custom rules)
|
|
51
52
|
- Configure agent memory and conversation settings
|
|
52
53
|
- Connect agents to functions, knowledge bases, and integrations
|
|
53
54
|
|
|
@@ -76,6 +77,10 @@ The AI Studio provides visual tools for building and testing AI agents:
|
|
|
76
77
|
- Search and preview indexed content
|
|
77
78
|
- Monitor indexing status
|
|
78
79
|
|
|
80
|
+
### Knowledge Graph
|
|
81
|
+
- Enable via the **Knowledge Graph** toggle when creating a knowledge base with **Vector Store** set to `mongoAtlas` (disabled for other stores — the store is immutable after creation), or later by editing an Atlas knowledge base
|
|
82
|
+
- Knowledge base page shows a **Knowledge Graph** card: last build time, documents changed since it, next automatic build ETA, and a **Build now** button (runs a structural build)
|
|
83
|
+
|
|
79
84
|
## Integrations Setup
|
|
80
85
|
|
|
81
86
|
### Database Connections
|
|
@@ -151,7 +156,7 @@ const agent = squid.ai().agent('my-agent');
|
|
|
151
156
|
await agent.upsert({
|
|
152
157
|
description: 'Customer support agent',
|
|
153
158
|
options: {
|
|
154
|
-
model: 'gpt-
|
|
159
|
+
model: 'gpt-5.6-terra',
|
|
155
160
|
instructions: 'You are a helpful support agent...'
|
|
156
161
|
}
|
|
157
162
|
});
|
|
@@ -8,6 +8,8 @@ Squid provides database functionality similar to Firestore but more powerful, wi
|
|
|
8
8
|
- CRUD Operations
|
|
9
9
|
- Real-time Subscriptions
|
|
10
10
|
- Query Operators
|
|
11
|
+
- Querying by Document ID
|
|
12
|
+
- Deleting by Query
|
|
11
13
|
- OR Queries
|
|
12
14
|
- Join Queries
|
|
13
15
|
- Dereference
|
|
@@ -231,6 +233,52 @@ const sorted = await users.query()
|
|
|
231
233
|
|
|
232
234
|
**Note:** `offset()` does NOT exist - use `paginate()` for pagination.
|
|
233
235
|
|
|
236
|
+
## Querying by Document ID
|
|
237
|
+
|
|
238
|
+
Filter on the document id with the `docId()` / `docIds()` shortcuts, or with `__docId__` (the
|
|
239
|
+
`DOC_ID_FIELD` constant) in `eq`/`neq`/`in`/`nin`/`where`. This works for both single- and
|
|
240
|
+
composite-key collections: pass the primitive for a single-field key, or the key object for a
|
|
241
|
+
composite one.
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { DOC_ID_FIELD } from '@squidcloud/client';
|
|
245
|
+
|
|
246
|
+
// Single-key collection
|
|
247
|
+
const one = await users.query().docId('user-1').snapshot();
|
|
248
|
+
const some = await users.query().docIds(['user-1', 'user-2']).snapshot();
|
|
249
|
+
|
|
250
|
+
// Composite-key collection - pass the key object
|
|
251
|
+
const line = await orderLines.query().docId({ orderId: 'o-1', lineNo: 3 }).snapshot();
|
|
252
|
+
|
|
253
|
+
// Equivalent explicit forms
|
|
254
|
+
await users.query().eq(DOC_ID_FIELD, 'user-1').snapshot();
|
|
255
|
+
await users.query().in(DOC_ID_FIELD, ['user-1', 'user-2']).snapshot();
|
|
256
|
+
await users.query().where(DOC_ID_FIELD, '==', 'user-1').snapshot();
|
|
257
|
+
await users.query().where(DOC_ID_FIELD, 'in', ['user-1', 'user-2']).snapshot();
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`docId()` is `eq(DOC_ID_FIELD, ...)` and `docIds()` is `in(DOC_ID_FIELD, ...)` — use them instead of
|
|
261
|
+
fetching a page and filtering client-side.
|
|
262
|
+
|
|
263
|
+
## Deleting by Query
|
|
264
|
+
|
|
265
|
+
`delete()` on a query deletes every document the query selects and returns how many were selected.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const { deletedCount } = await users.query()
|
|
269
|
+
.eq('status', 'inactive')
|
|
270
|
+
.lt('lastLoginAt', cutoff)
|
|
271
|
+
.delete();
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`sortBy`, `limit`, and `limitBy` are rejected — `delete()` throws if the query carries any of them.
|
|
275
|
+
A query whose `in` condition is an empty array selects nothing, so it deletes nothing and returns
|
|
276
|
+
`{ deletedCount: 0 }`. An empty `nin` behaves the opposite way: it contributes no condition at all,
|
|
277
|
+
so it does not restrict the delete — `users.query().nin('id', []).delete()` deletes everything the
|
|
278
|
+
remaining conditions select, the whole collection when there are none. Narrow an empty exclusion list
|
|
279
|
+
in your own code before calling `delete()`. Deletes go through the collection's `delete` security
|
|
280
|
+
rules, same as `doc.delete()`.
|
|
281
|
+
|
|
234
282
|
## OR Queries
|
|
235
283
|
|
|
236
284
|
Combine multiple queries with OR logic:
|
|
@@ -7,7 +7,7 @@ OpenAI-specific features in Squid. For general AI usage, see [ai.md](ai.md).
|
|
|
7
7
|
```typescript
|
|
8
8
|
import {
|
|
9
9
|
OPENAI_CHAT_MODEL_NAMES, // Chat models
|
|
10
|
-
OPENAI_IMAGE_MODEL_NAMES, //
|
|
10
|
+
OPENAI_IMAGE_MODEL_NAMES, // gpt-image-*
|
|
11
11
|
OPENAI_AUDIO_TRANSCRIPTION_MODEL_NAMES, // Whisper
|
|
12
12
|
OPENAI_AUDIO_CREATE_SPEECH_MODEL_NAMES, // TTS
|
|
13
13
|
OPENAI_EMBEDDINGS_MODEL_NAMES, // Embeddings
|
|
@@ -10,7 +10,7 @@ Squid provides a comprehensive security model using backend decorators. Every op
|
|
|
10
10
|
- Storage Security
|
|
11
11
|
- API Security
|
|
12
12
|
- Native Query Security
|
|
13
|
-
- AI Security
|
|
13
|
+
- AI Security (including @secureLangGraph)
|
|
14
14
|
- Distributed Lock Security
|
|
15
15
|
- GraphQL Security
|
|
16
16
|
- Authentication Patterns
|
|
@@ -132,6 +132,34 @@ context.affectsPath(path) // Check if specific field path was modified
|
|
|
132
132
|
|
|
133
133
|
Both types are exported from `@squidcloud/backend`.
|
|
134
134
|
|
|
135
|
+
### @publicCollection
|
|
136
|
+
|
|
137
|
+
A **class** decorator that declares reads of a collection as intentionally public. Public reads
|
|
138
|
+
bypass security-rule evaluation entirely: Core serves them without calling backend code, so any
|
|
139
|
+
client can read the collection, **including unauthenticated ones**. Use it only for data that is
|
|
140
|
+
safe to expose to everyone; prefer a `@secureCollection` read rule when access depends on the caller.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { SquidService, publicCollection, secureCollection, MutationContext } from '@squidcloud/backend';
|
|
144
|
+
|
|
145
|
+
// Anyone can read `articles`, but only authenticated users can write to it.
|
|
146
|
+
@publicCollection('articles', 'read')
|
|
147
|
+
export class ArticleService extends SquidService {
|
|
148
|
+
@secureCollection('articles', 'write')
|
|
149
|
+
async secureArticlesWrite(context: MutationContext): Promise<boolean> {
|
|
150
|
+
return this.isAuthenticated();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- Signature: `publicCollection(collectionName, 'read', integrationId?)`. `integrationId` defaults to
|
|
156
|
+
the built-in database.
|
|
157
|
+
- Only `read` is supported. Writes always go through their security rules, so a public-read
|
|
158
|
+
collection can still restrict `insert`/`update`/`delete`/`write`.
|
|
159
|
+
- **Secure wins on conflict:** if a `@secureCollection` or `@secureDatabase` `read` (or `all`) rule
|
|
160
|
+
also applies to the same collection, that rule takes precedence at runtime and the public
|
|
161
|
+
declaration is ignored.
|
|
162
|
+
|
|
135
163
|
## Queue Security
|
|
136
164
|
|
|
137
165
|
### @secureTopic
|
|
@@ -270,6 +298,51 @@ allowAllAgents(): boolean {
|
|
|
270
298
|
}
|
|
271
299
|
```
|
|
272
300
|
|
|
301
|
+
### @secureLangGraph
|
|
302
|
+
|
|
303
|
+
Secures LangGraph invocations made with `squid.langGraph(graphId)` (see
|
|
304
|
+
[client.md](client.md#langgraph)).
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { SquidService, secureLangGraph, SecureLangGraphContext } from '@squidcloud/backend';
|
|
308
|
+
|
|
309
|
+
// Secure a specific graph
|
|
310
|
+
@secureLangGraph('support-graph')
|
|
311
|
+
allowSupportGraph(context: SecureLangGraphContext): boolean {
|
|
312
|
+
// context.operation: 'invoke' | 'resume' | 'getState' | 'deleteThread'
|
|
313
|
+
const userId = this.getUserAuth()?.userId;
|
|
314
|
+
if (!userId) return false;
|
|
315
|
+
// This app mints thread ids as `${userId}:${conversation}` and passes one on every call, so
|
|
316
|
+
// ownership is derived from threadId. Check it on every operation, not just deleteThread:
|
|
317
|
+
// threadId is a client-supplied field everywhere and threads carry no owner of their own, so
|
|
318
|
+
// this rule is all that stops one authenticated user from reading, extending, or deleting
|
|
319
|
+
// another's thread. An invoke that omits threadId is refused too: the runtime would mint a bare
|
|
320
|
+
// UUID, and no later call on that thread could satisfy this rule.
|
|
321
|
+
// Never derive ownership from context.input — it is populated on invoke only, so the comparison
|
|
322
|
+
// would be undefined === undefined, which passes for exactly the callers it should refuse.
|
|
323
|
+
return !!context.threadId && context.threadId.startsWith(`${userId}:`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Secure all graphs
|
|
327
|
+
@secureLangGraph()
|
|
328
|
+
allowAllGraphs(context: SecureLangGraphContext): boolean {
|
|
329
|
+
return this.isAuthenticated();
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`SecureLangGraphContext` carries `operation`, `graphId`, `threadId`, `input`, and `resumePayload`.
|
|
334
|
+
Only the fields the triggering request itself carries are populated: `input` on `invoke` only,
|
|
335
|
+
`resumePayload` on `resume` only, and `threadId` on everything except an `invoke` that starts a new
|
|
336
|
+
thread. A rule that reads a field its operation does not carry sees `undefined`, so write ownership
|
|
337
|
+
checks that cannot pass on `undefined` — otherwise an unauthenticated caller, whose `getUserAuth()`
|
|
338
|
+
is also `undefined`, is the one the rule lets through.
|
|
339
|
+
|
|
340
|
+
A thread has no owner at the storage layer and `threadId` reaches the rule straight from the client
|
|
341
|
+
body on every operation, so the `@secureLangGraph` rule is the only per-thread access control there
|
|
342
|
+
is. The `allowAllGraphs` shape above authenticates the caller but does not scope them to their own
|
|
343
|
+
threads — enough for a single-tenant graph, not for one where users must not see each other's
|
|
344
|
+
conversations.
|
|
345
|
+
|
|
273
346
|
## Distributed Lock Security
|
|
274
347
|
|
|
275
348
|
### @secureDistributedLock
|
|
@@ -12,7 +12,7 @@ Each Squid SaaS connector provides built-in AI functions that AI agents can call
|
|
|
12
12
|
await squid.ai().agent('my-agent').upsert({
|
|
13
13
|
description: 'Support agent',
|
|
14
14
|
options: {
|
|
15
|
-
model: 'gpt-5-
|
|
15
|
+
model: 'gpt-5.6-terra',
|
|
16
16
|
instructions: 'You are a support agent that can search tickets and send messages.'
|
|
17
17
|
}
|
|
18
18
|
});
|
|
@@ -164,12 +164,13 @@ function TaskList({ userId }: { userId: string }) {
|
|
|
164
164
|
const tasksCollection = useCollection<Task>('tasks');
|
|
165
165
|
const query = tasksCollection.query().where('userId', '==', userId);
|
|
166
166
|
|
|
167
|
-
const { loading, data, error } = useQuery(query, {
|
|
167
|
+
const { loading, data, error, enabled } = useQuery(query, {
|
|
168
168
|
enabled: !!userId, // Only run when userId is set
|
|
169
169
|
subscribe: true, // Real-time updates (default)
|
|
170
170
|
initialData: [], // Initial data before first load
|
|
171
171
|
}, [userId]); // Re-subscribe when userId changes
|
|
172
172
|
|
|
173
|
+
if (!enabled) return <div>No user selected</div>; // Check before `loading`
|
|
173
174
|
if (loading) return <div>Loading...</div>;
|
|
174
175
|
if (error) return <div>Error: {error.message}</div>;
|
|
175
176
|
|
|
@@ -201,9 +202,20 @@ interface QueryType<T> {
|
|
|
201
202
|
loading: boolean;
|
|
202
203
|
data: Array<T>;
|
|
203
204
|
error: any;
|
|
205
|
+
enabled: boolean; // Mirrors options.enabled
|
|
204
206
|
}
|
|
205
207
|
```
|
|
206
208
|
|
|
209
|
+
**`loading` stays `true` while the query is disabled.** A disabled query never subscribes, so it
|
|
210
|
+
never resolves — gate on `enabled` first, or `if (loading)` renders a spinner forever:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const { loading, data, error, enabled } = useQuery(query, { enabled: !!userId }, [userId]);
|
|
214
|
+
|
|
215
|
+
if (!enabled) return <SelectUserPrompt />; // Not "loading" - just not running
|
|
216
|
+
if (loading) return <Spinner />;
|
|
217
|
+
```
|
|
218
|
+
|
|
207
219
|
**Single Snapshot vs Real-time:**
|
|
208
220
|
```typescript
|
|
209
221
|
// Real-time updates (subscribe: true) - default
|
|
@@ -940,7 +952,7 @@ function AdvancedFeatures() {
|
|
|
940
952
|
const agent = squid.ai().agent('new-agent');
|
|
941
953
|
await agent.upsert({
|
|
942
954
|
description: 'My new agent',
|
|
943
|
-
options: { model: 'gpt-
|
|
955
|
+
options: { model: 'gpt-5.6-terra' },
|
|
944
956
|
});
|
|
945
957
|
};
|
|
946
958
|
|
|
@@ -1054,13 +1066,18 @@ const { data } = useQuery(
|
|
|
1054
1066
|
### 2. Use `enabled` for Conditional Fetching
|
|
1055
1067
|
|
|
1056
1068
|
```typescript
|
|
1057
|
-
// GOOD: Prevent unnecessary queries
|
|
1058
|
-
const { data } = useQuery(query, { enabled: !!userId }, [userId]);
|
|
1069
|
+
// GOOD: Prevent unnecessary queries, and distinguish "disabled" from "loading"
|
|
1070
|
+
const { data, loading, enabled } = useQuery(query, { enabled: !!userId }, [userId]);
|
|
1071
|
+
if (!enabled) return <EmptyState />; // `loading` is still true here
|
|
1072
|
+
if (loading) return <Spinner />;
|
|
1059
1073
|
|
|
1060
1074
|
// BAD: Query runs even when userId is null
|
|
1061
1075
|
const { data } = useQuery(query, {}, [userId]);
|
|
1062
1076
|
```
|
|
1063
1077
|
|
|
1078
|
+
Only `useQuery` returns `enabled`. For the other hooks, track the same condition yourself rather
|
|
1079
|
+
than relying on `loading` to go false while they are disabled.
|
|
1080
|
+
|
|
1064
1081
|
### 3. Provide Type Parameters
|
|
1065
1082
|
|
|
1066
1083
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squidcloud/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.489",
|
|
4
4
|
"description": "The Squid CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@squidcloud/local-backend": "^1.0.
|
|
31
|
+
"@squidcloud/local-backend": "^1.0.489",
|
|
32
32
|
"adm-zip": "^0.5.16",
|
|
33
33
|
"copy-webpack-plugin": "^14.0.0",
|
|
34
34
|
"decompress": "^4.2.1",
|