@seaverse/data-service-sdk 0.5.1 → 0.6.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/README.md CHANGED
@@ -10,6 +10,7 @@ SeaVerse Data Service SDK for accessing Firestore with secure token management a
10
10
  - 🔒 Automatic data isolation by app_id
11
11
  - 📝 TypeScript support with full type definitions
12
12
  - 🤖 LLM-friendly documentation with clear examples
13
+ - 🛡️ Path helper functions to prevent permission-denied errors
13
14
 
14
15
  ## Three-Tier Permission Model
15
16
 
@@ -130,12 +131,116 @@ const { DataServiceClient } = require('@seaverse/data-service-sdk');
130
131
  import { DataServiceClient } from '@seaverse/data-service-sdk';
131
132
  ```
132
133
 
134
+ ## Path Helper Functions (🚨 Recommended for LLM)
135
+
136
+ To prevent `permission-denied` errors caused by incorrect paths, we provide helper functions that generate the correct Firestore paths automatically.
137
+
138
+ ### Why Use Path Helpers?
139
+
140
+ **Problem**: LLM or developers might accidentally use wrong paths:
141
+ ```typescript
142
+ // ❌ WRONG - Will cause permission-denied!
143
+ collection(db, `apps/${appId}/publicArticles`) // Not matching security rules!
144
+ collection(db, `apps/${appId}/users/${userId}/articles`) // Not matching security rules!
145
+ ```
146
+
147
+ **Solution**: Use path helper functions:
148
+ ```typescript
149
+ import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
150
+
151
+ // ✅ CORRECT - Guaranteed to match security rules
152
+ collection(db, getPublicDataPath(appId, 'posts')) // → appData/{appId}/publicData/posts
153
+ collection(db, getUserDataPath(appId, userId, 'notes')) // → appData/{appId}/userData/{userId}/notes
154
+ ```
155
+
156
+ ### Available Path Helpers
157
+
158
+ ```typescript
159
+ import {
160
+ getPublicReadPath, // For read-only public data
161
+ getPublicDataPath, // For public read/write data
162
+ getUserDataPath, // For private user data
163
+ getPublicReadDocPath, // For specific public document
164
+ getPublicDataDocPath, // For specific public data document
165
+ getUserDataDocPath, // For specific user document
166
+ PathBuilder // For advanced path building
167
+ } from '@seaverse/data-service-sdk';
168
+ ```
169
+
170
+ ### Basic Usage Examples
171
+
172
+ ```typescript
173
+ // Public data that everyone can read/write
174
+ const postsPath = getPublicDataPath(appId, 'posts');
175
+ await addDoc(collection(db, postsPath), {
176
+ _appId: appId,
177
+ _createdAt: serverTimestamp(),
178
+ _createdBy: userId,
179
+ title: 'My Post'
180
+ });
181
+
182
+ // Private user data
183
+ const notesPath = getUserDataPath(appId, userId, 'notes');
184
+ await addDoc(collection(db, notesPath), {
185
+ _appId: appId,
186
+ _createdAt: serverTimestamp(),
187
+ _createdBy: userId,
188
+ content: 'Private note'
189
+ });
190
+
191
+ // Public read-only data (admin writes only)
192
+ const configPath = getPublicReadPath(appId, 'config');
193
+ const configs = await getDocs(collection(db, configPath));
194
+
195
+ // Access specific document
196
+ const docPath = getPublicDataDocPath(appId, 'posts', 'post-123');
197
+ const docSnap = await getDoc(doc(db, docPath));
198
+ ```
199
+
200
+ ### Advanced: PathBuilder
201
+
202
+ For complex path construction:
203
+
204
+ ```typescript
205
+ import { PathBuilder } from '@seaverse/data-service-sdk';
206
+
207
+ const builder = new PathBuilder(appId);
208
+
209
+ // Build collection path
210
+ const path = builder.publicData('posts').build();
211
+ // Returns: 'appData/my-app/publicData/posts'
212
+
213
+ // Build document path
214
+ const docPath = builder.publicData('posts').doc('post-123').build();
215
+ // Returns: 'appData/my-app/publicData/posts/post-123'
216
+
217
+ // Build user data path
218
+ const userPath = builder.userData(userId, 'notes').build();
219
+ // Returns: 'appData/my-app/userData/user-123/notes'
220
+ ```
221
+
222
+ ### Error Prevention
223
+
224
+ Path helpers validate inputs to prevent common mistakes:
225
+
226
+ ```typescript
227
+ // ❌ These will throw errors:
228
+ getPublicDataPath('my-app', 'posts/comments'); // Error: cannot contain /
229
+ getPublicDataPath('my-app', ''); // Error: must be non-empty string
230
+ getUserDataPath('my-app', '', 'notes'); // Error: userId must be non-empty
231
+ ```
232
+
133
233
  ## Quick Start
134
234
 
135
235
  ### 🚀 Easiest Way (Recommended - Auto Firebase Setup)
136
236
 
137
237
  ```typescript
138
- import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
238
+ import {
239
+ DataServiceClient,
240
+ initializeWithToken,
241
+ getPublicDataPath, // 🛡️ Use path helpers!
242
+ getUserDataPath // 🛡️ Use path helpers!
243
+ } from '@seaverse/data-service-sdk';
139
244
  import { AuthClient } from '@seaverse/auth-sdk';
140
245
  import { collection, addDoc, getDocs, serverTimestamp } from 'firebase/firestore';
141
246
 
@@ -156,10 +261,11 @@ const tokenResponse = await dataClient.generateFirestoreToken({
156
261
  // Step 3: Auto-initialize Firebase (ONE LINE!)
157
262
  const { db, appId, userId } = await initializeWithToken(tokenResponse);
158
263
 
159
- // Step 4: Use Firestore directly!
264
+ // Step 4: Use Firestore directly with path helpers!
160
265
 
161
266
  // Write to publicData (everyone can write)
162
- await addDoc(collection(db, `appData/${appId}/publicData/posts`), {
267
+ const postsPath = getPublicDataPath(appId, 'posts'); // 🛡️ Safe path!
268
+ await addDoc(collection(db, postsPath), {
163
269
  _appId: appId, // REQUIRED
164
270
  _createdAt: serverTimestamp(), // REQUIRED
165
271
  _createdBy: userId, // REQUIRED
@@ -168,13 +274,14 @@ await addDoc(collection(db, `appData/${appId}/publicData/posts`), {
168
274
  });
169
275
 
170
276
  // Read from publicData
171
- const snapshot = await getDocs(collection(db, `appData/${appId}/publicData/posts`));
277
+ const snapshot = await getDocs(collection(db, postsPath));
172
278
  snapshot.forEach(doc => {
173
279
  console.log(doc.id, doc.data());
174
280
  });
175
281
 
176
282
  // Write to userData (private)
177
- await addDoc(collection(db, `appData/${appId}/userData/${userId}/notes`), {
283
+ const notesPath = getUserDataPath(appId, userId, 'notes'); // 🛡️ Safe path!
284
+ await addDoc(collection(db, notesPath), {
178
285
  _appId: appId, // REQUIRED
179
286
  _createdAt: serverTimestamp(), // REQUIRED
180
287
  _createdBy: userId, // REQUIRED
@@ -186,7 +293,11 @@ await addDoc(collection(db, `appData/${appId}/userData/${userId}/notes`), {
186
293
  ### 👤 For Guest Users (Even Simpler!)
187
294
 
188
295
  ```typescript
189
- import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
296
+ import {
297
+ DataServiceClient,
298
+ initializeWithToken,
299
+ getPublicDataPath // 🛡️ Use path helpers!
300
+ } from '@seaverse/data-service-sdk';
190
301
  import { collection, addDoc, serverTimestamp } from 'firebase/firestore';
191
302
 
192
303
  // Step 1: Get guest token (no authentication needed!)
@@ -199,7 +310,8 @@ const tokenResponse = await dataClient.generateGuestFirestoreToken({
199
310
  const { db, appId, userId } = await initializeWithToken(tokenResponse);
200
311
 
201
312
  // Step 3: Guest can write to publicData
202
- await addDoc(collection(db, `appData/${appId}/publicData/comments`), {
313
+ const commentsPath = getPublicDataPath(appId, 'comments'); // 🛡️ Safe path!
314
+ await addDoc(collection(db, commentsPath), {
203
315
  _appId: appId,
204
316
  _createdAt: serverTimestamp(),
205
317
  _createdBy: userId, // Guest user ID (e.g., 'guest-abc123')
@@ -237,6 +349,20 @@ const app = initializeApp({
237
349
 
238
350
  const auth = getAuth(app);
239
351
  await signInWithCustomToken(auth, tokenResponse.custom_token);
352
+
353
+ // ⚠️ IMPORTANT: Must specify database_id!
354
+ const db = getFirestore(app, tokenResponse.database_id);
355
+ ```
356
+
357
+ **🚨 CRITICAL: Always Specify database_id**
358
+
359
+ When initializing Firestore, you MUST pass the `database_id` from the token response:
360
+
361
+ ```typescript
362
+ // ✅ CORRECT - Specify database_id
363
+ const db = getFirestore(app, tokenResponse.database_id);
364
+
365
+ // ❌ WRONG - Will try to use "(default)" database which may not exist
240
366
  const db = getFirestore(app);
241
367
  ```
242
368
 
@@ -552,7 +678,19 @@ import type {
552
678
 
553
679
  When using this SDK with LLM-generated code:
554
680
 
555
- 1. **Always include required fields:**
681
+ 1. **🛡️ MOST IMPORTANT: Use path helper functions to avoid permission-denied errors:**
682
+ ```typescript
683
+ import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
684
+
685
+ // ✅ CORRECT - Use helpers
686
+ const path = getPublicDataPath(appId, 'posts');
687
+ await addDoc(collection(db, path), { ... });
688
+
689
+ // ❌ WRONG - Manual paths may be incorrect
690
+ await addDoc(collection(db, `apps/${appId}/posts`), { ... });
691
+ ```
692
+
693
+ 2. **Always include required fields:**
556
694
  ```typescript
557
695
  {
558
696
  _appId: appId, // From token response
@@ -561,16 +699,26 @@ When using this SDK with LLM-generated code:
561
699
  }
562
700
  ```
563
701
 
564
- 2. **Use correct data paths:**
565
- - publicRead: `appData/${appId}/publicRead/{collection}/{docId}`
566
- - publicData: `appData/${appId}/publicData/{collection}/{docId}`
567
- - userData: `appData/${appId}/userData/${userId}/{collection}/{docId}`
702
+ 3. **Use correct data paths with helpers:**
703
+ ```typescript
704
+ // Use path helpers instead of manual strings!
705
+ import { getPublicReadPath, getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
706
+
707
+ // publicRead
708
+ const configPath = getPublicReadPath(appId, 'config');
709
+
710
+ // publicData
711
+ const postsPath = getPublicDataPath(appId, 'posts');
712
+
713
+ // userData
714
+ const notesPath = getUserDataPath(appId, userId, 'notes');
715
+ ```
568
716
 
569
- 3. **Handle token expiration:**
717
+ 4. **Handle token expiration:**
570
718
  - Tokens expire after 1 hour (3600 seconds)
571
719
  - Check `expires_in` field and refresh when needed
572
720
 
573
- 4. **Use serverTimestamp() for timestamps:**
721
+ 5. **Use serverTimestamp() for timestamps:**
574
722
  ```typescript
575
723
  import { serverTimestamp } from 'firebase/firestore';
576
724
 
@@ -579,7 +727,7 @@ When using this SDK with LLM-generated code:
579
727
  }
580
728
  ```
581
729
 
582
- 5. **Separate guest and authenticated flows:**
730
+ 6. **Separate guest and authenticated flows:**
583
731
  - Use `generateGuestFirestoreToken()` for anonymous users
584
732
  - Use `generateFirestoreToken()` for logged-in users
585
733
 
package/dist/browser.js CHANGED
@@ -4251,8 +4251,9 @@ async function initializeWithToken(tokenResponse) {
4251
4251
  // Sign in with custom token
4252
4252
  const auth = getAuth(app);
4253
4253
  await signInWithCustomToken(auth, tokenResponse.custom_token);
4254
- // Get Firestore instance
4255
- const db = getFirestore(app);
4254
+ // Get Firestore instance with correct database ID
4255
+ // IMPORTANT: Must specify database_id, not just use default!
4256
+ const db = getFirestore(app, tokenResponse.database_id);
4256
4257
  return {
4257
4258
  app,
4258
4259
  auth,
@@ -4262,5 +4263,259 @@ async function initializeWithToken(tokenResponse) {
4262
4263
  };
4263
4264
  }
4264
4265
 
4265
- export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, getFirebaseConfig, initializeWithToken };
4266
+ /**
4267
+ * Firestore Path Helper Functions
4268
+ *
4269
+ * These helper functions generate correct Firestore paths that match
4270
+ * the security rules. Use these instead of manually constructing paths
4271
+ * to avoid permission-denied errors.
4272
+ *
4273
+ * 🚨 IMPORTANT: These paths are designed to work with SeaVerse Firestore Rules
4274
+ *
4275
+ * Permission Levels:
4276
+ * - publicRead: Read-only for all users, write for admins only
4277
+ * - publicData: Read/write for all authenticated users
4278
+ * - userData: Read/write only for the data owner
4279
+ */
4280
+ /**
4281
+ * Generate path for publicRead data (read-only for users, write for admins)
4282
+ *
4283
+ * @param appId - Your application ID
4284
+ * @param collection - Collection name (e.g., 'announcements', 'config')
4285
+ * @returns Firestore path string
4286
+ *
4287
+ * @example
4288
+ * ```typescript
4289
+ * // Read system announcements
4290
+ * const path = getPublicReadPath('my-app', 'announcements');
4291
+ * // Returns: 'appData/my-app/publicRead/announcements'
4292
+ *
4293
+ * const snapshot = await getDocs(collection(db, path));
4294
+ * ```
4295
+ */
4296
+ function getPublicReadPath(appId, collectionName) {
4297
+ validateSegment('appId', appId);
4298
+ validateSegment('collectionName', collectionName);
4299
+ return `appData/${appId}/publicRead/${collectionName}`;
4300
+ }
4301
+ /**
4302
+ * Generate path for publicData (read/write for all authenticated users)
4303
+ *
4304
+ * @param appId - Your application ID
4305
+ * @param collection - Collection name (e.g., 'posts', 'comments')
4306
+ * @returns Firestore path string
4307
+ *
4308
+ * @example
4309
+ * ```typescript
4310
+ * // Write a public post
4311
+ * const path = getPublicDataPath('my-app', 'posts');
4312
+ * // Returns: 'appData/my-app/publicData/posts'
4313
+ *
4314
+ * await addDoc(collection(db, path), {
4315
+ * _appId: appId,
4316
+ * _createdAt: serverTimestamp(),
4317
+ * _createdBy: userId,
4318
+ * title: 'My Post'
4319
+ * });
4320
+ * ```
4321
+ */
4322
+ function getPublicDataPath(appId, collectionName) {
4323
+ validateSegment('appId', appId);
4324
+ validateSegment('collectionName', collectionName);
4325
+ return `appData/${appId}/publicData/${collectionName}`;
4326
+ }
4327
+ /**
4328
+ * Generate path for userData (private, read/write only by owner)
4329
+ *
4330
+ * @param appId - Your application ID
4331
+ * @param userId - User ID who owns this data
4332
+ * @param collection - Collection name (e.g., 'notes', 'settings')
4333
+ * @returns Firestore path string
4334
+ *
4335
+ * @example
4336
+ * ```typescript
4337
+ * // Write private user notes
4338
+ * const path = getUserDataPath('my-app', 'user-123', 'notes');
4339
+ * // Returns: 'appData/my-app/userData/user-123/notes'
4340
+ *
4341
+ * await addDoc(collection(db, path), {
4342
+ * _appId: appId,
4343
+ * _createdAt: serverTimestamp(),
4344
+ * _createdBy: userId,
4345
+ * content: 'Private note'
4346
+ * });
4347
+ * ```
4348
+ */
4349
+ function getUserDataPath(appId, userId, collectionName) {
4350
+ validateSegment('appId', appId);
4351
+ validateSegment('userId', userId);
4352
+ validateSegment('collectionName', collectionName);
4353
+ return `appData/${appId}/userData/${userId}/${collectionName}`;
4354
+ }
4355
+ /**
4356
+ * Generate path for a specific document in publicRead
4357
+ *
4358
+ * @param appId - Your application ID
4359
+ * @param collection - Collection name
4360
+ * @param docId - Document ID
4361
+ * @returns Firestore document path string
4362
+ *
4363
+ * @example
4364
+ * ```typescript
4365
+ * const path = getPublicReadDocPath('my-app', 'announcements', 'announcement-1');
4366
+ * // Returns: 'appData/my-app/publicRead/announcements/announcement-1'
4367
+ *
4368
+ * const docSnap = await getDoc(doc(db, path));
4369
+ * ```
4370
+ */
4371
+ function getPublicReadDocPath(appId, collectionName, docId) {
4372
+ validateSegment('appId', appId);
4373
+ validateSegment('collectionName', collectionName);
4374
+ validateSegment('docId', docId);
4375
+ return `appData/${appId}/publicRead/${collectionName}/${docId}`;
4376
+ }
4377
+ /**
4378
+ * Generate path for a specific document in publicData
4379
+ *
4380
+ * @param appId - Your application ID
4381
+ * @param collection - Collection name
4382
+ * @param docId - Document ID
4383
+ * @returns Firestore document path string
4384
+ *
4385
+ * @example
4386
+ * ```typescript
4387
+ * const path = getPublicDataDocPath('my-app', 'posts', 'post-123');
4388
+ * // Returns: 'appData/my-app/publicData/posts/post-123'
4389
+ *
4390
+ * const docSnap = await getDoc(doc(db, path));
4391
+ * ```
4392
+ */
4393
+ function getPublicDataDocPath(appId, collectionName, docId) {
4394
+ validateSegment('appId', appId);
4395
+ validateSegment('collectionName', collectionName);
4396
+ validateSegment('docId', docId);
4397
+ return `appData/${appId}/publicData/${collectionName}/${docId}`;
4398
+ }
4399
+ /**
4400
+ * Generate path for a specific document in userData
4401
+ *
4402
+ * @param appId - Your application ID
4403
+ * @param userId - User ID who owns this data
4404
+ * @param collection - Collection name
4405
+ * @param docId - Document ID
4406
+ * @returns Firestore document path string
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * const path = getUserDataDocPath('my-app', 'user-123', 'notes', 'note-456');
4411
+ * // Returns: 'appData/my-app/userData/user-123/notes/note-456'
4412
+ *
4413
+ * const docSnap = await getDoc(doc(db, path));
4414
+ * ```
4415
+ */
4416
+ function getUserDataDocPath(appId, userId, collectionName, docId) {
4417
+ validateSegment('appId', appId);
4418
+ validateSegment('userId', userId);
4419
+ validateSegment('collectionName', collectionName);
4420
+ validateSegment('docId', docId);
4421
+ return `appData/${appId}/userData/${userId}/${collectionName}/${docId}`;
4422
+ }
4423
+ /**
4424
+ * Validate a path segment to ensure it doesn't contain invalid characters
4425
+ *
4426
+ * @param name - Parameter name for error messages
4427
+ * @param value - The segment value to validate
4428
+ * @throws Error if the segment is invalid
4429
+ */
4430
+ function validateSegment(name, value) {
4431
+ if (!value || typeof value !== 'string') {
4432
+ throw new Error(`${name} must be a non-empty string`);
4433
+ }
4434
+ if (value.includes('/')) {
4435
+ throw new Error(`${name} cannot contain forward slashes (/). Got: "${value}"`);
4436
+ }
4437
+ if (value.trim() !== value) {
4438
+ throw new Error(`${name} cannot start or end with whitespace. Got: "${value}"`);
4439
+ }
4440
+ }
4441
+ /**
4442
+ * Path builder for advanced use cases
4443
+ * Provides a fluent interface for building Firestore paths
4444
+ *
4445
+ * @example
4446
+ * ```typescript
4447
+ * // Build a path step by step
4448
+ * const builder = new PathBuilder('my-app');
4449
+ * const path = builder.publicData('posts').build();
4450
+ * // Returns: 'appData/my-app/publicData/posts'
4451
+ *
4452
+ * // With document ID
4453
+ * const docPath = builder.publicData('posts').doc('post-123').build();
4454
+ * // Returns: 'appData/my-app/publicData/posts/post-123'
4455
+ * ```
4456
+ */
4457
+ class PathBuilder {
4458
+ constructor(appId) {
4459
+ this.appId = appId;
4460
+ this.segments = ['appData'];
4461
+ validateSegment('appId', appId);
4462
+ this.segments.push(appId);
4463
+ }
4464
+ /**
4465
+ * Add publicRead collection to path
4466
+ */
4467
+ publicRead(collectionName) {
4468
+ validateSegment('collectionName', collectionName);
4469
+ this.segments.push('publicRead', collectionName);
4470
+ return this;
4471
+ }
4472
+ /**
4473
+ * Add publicData collection to path
4474
+ */
4475
+ publicData(collectionName) {
4476
+ validateSegment('collectionName', collectionName);
4477
+ this.segments.push('publicData', collectionName);
4478
+ return this;
4479
+ }
4480
+ /**
4481
+ * Add userData collection to path
4482
+ */
4483
+ userData(userId, collectionName) {
4484
+ validateSegment('userId', userId);
4485
+ validateSegment('collectionName', collectionName);
4486
+ this.segments.push('userData', userId, collectionName);
4487
+ return this;
4488
+ }
4489
+ /**
4490
+ * Add document ID to path
4491
+ */
4492
+ doc(docId) {
4493
+ validateSegment('docId', docId);
4494
+ this.segments.push(docId);
4495
+ return this;
4496
+ }
4497
+ /**
4498
+ * Build the final path string
4499
+ */
4500
+ build() {
4501
+ return this.segments.join('/');
4502
+ }
4503
+ }
4504
+ /**
4505
+ * Common path patterns as constants for frequently used paths
4506
+ */
4507
+ const PATH_PATTERNS = {
4508
+ /**
4509
+ * Base pattern for all SeaVerse data
4510
+ */
4511
+ APP_DATA: 'appData',
4512
+ /**
4513
+ * Permission layers
4514
+ */
4515
+ PUBLIC_READ: 'publicRead',
4516
+ PUBLIC_DATA: 'publicData',
4517
+ USER_DATA: 'userData',
4518
+ };
4519
+
4520
+ export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, PATH_PATTERNS, PathBuilder, getFirebaseConfig, getPublicDataDocPath, getPublicDataPath, getPublicReadDocPath, getPublicReadPath, getUserDataDocPath, getUserDataPath, initializeWithToken };
4266
4521
  //# sourceMappingURL=browser.js.map