@hypercerts-org/sdk-core 0.10.0-beta.4 → 0.10.0-beta.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/CHANGELOG.md +226 -0
- package/README.md +309 -45
- package/dist/index.cjs +2749 -347
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1961 -182
- package/dist/index.mjs +3769 -1397
- package/dist/index.mjs.map +1 -1
- package/dist/lexicons.cjs +446 -34
- package/dist/lexicons.cjs.map +1 -1
- package/dist/lexicons.d.ts +248 -8
- package/dist/lexicons.mjs +422 -11
- package/dist/lexicons.mjs.map +1 -1
- package/dist/testing.d.ts +56 -9
- package/dist/types.cjs +89 -9
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +438 -97
- package/dist/types.mjs +89 -9
- package/dist/types.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -3,9 +3,41 @@
|
|
|
3
3
|
var oauthClientNode = require('@atproto/oauth-client-node');
|
|
4
4
|
var zod = require('zod');
|
|
5
5
|
var api = require('@atproto/api');
|
|
6
|
-
var
|
|
6
|
+
var lexicon$1 = require('@atproto/lexicon');
|
|
7
7
|
var lexicon = require('@hypercerts-org/lexicon');
|
|
8
|
-
require('
|
|
8
|
+
var eventemitter3 = require('eventemitter3');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Type guard to check if a URL is a loopback address.
|
|
12
|
+
*
|
|
13
|
+
* Loopback addresses are used for local development and testing.
|
|
14
|
+
* They include localhost, 127.0.0.1, and [::1] (IPv6 loopback).
|
|
15
|
+
*
|
|
16
|
+
* @param url - The URL to check
|
|
17
|
+
* @returns True if the URL is a loopback address
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* isLoopbackUrl('http://localhost:3000'); // true
|
|
22
|
+
* isLoopbackUrl('http://127.0.0.1:8080'); // true
|
|
23
|
+
* isLoopbackUrl('http://[::1]:3000'); // true
|
|
24
|
+
* isLoopbackUrl('http://example.com'); // false
|
|
25
|
+
* isLoopbackUrl('https://localhost:3000'); // false (must be http)
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function isLoopbackUrl(url) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
if (parsed.protocol !== "http:") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
35
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
9
41
|
|
|
10
42
|
/**
|
|
11
43
|
* Base error class for all SDK errors.
|
|
@@ -599,24 +631,47 @@ const IdentityAttrSchema = zod.z.enum(["handle", "*"]);
|
|
|
599
631
|
/**
|
|
600
632
|
* Zod schema for MIME type patterns.
|
|
601
633
|
*
|
|
602
|
-
* Validates MIME type strings
|
|
634
|
+
* Validates MIME type strings used in blob permissions.
|
|
635
|
+
* Supports standard MIME types and wildcard patterns per ATProto spec.
|
|
636
|
+
*
|
|
637
|
+
* **References:**
|
|
638
|
+
* - ATProto Permission Spec: https://atproto.com/specs/permission (blob resource)
|
|
639
|
+
* - RFC 2045 (MIME): https://www.rfc-editor.org/rfc/rfc2045 (token definition)
|
|
640
|
+
* - IANA Media Types: https://www.iana.org/assignments/media-types/
|
|
641
|
+
*
|
|
642
|
+
* **Implementation:**
|
|
643
|
+
* This is a "good enough" validation that allows common real-world MIME types:
|
|
644
|
+
* - Type: letters, digits (e.g., "3gpp")
|
|
645
|
+
* - Subtype: letters, digits, hyphens, plus signs, dots, underscores, wildcards
|
|
646
|
+
* - Examples: "image/png", "application/vnd.api+json", "video/*", "clue_info+xml"
|
|
647
|
+
*
|
|
648
|
+
* Note: We use a simplified regex rather than full RFC 2045 token validation
|
|
649
|
+
* for practicality. Zod v4 has native MIME support (z.file().mime()) but would
|
|
650
|
+
* require a larger migration effort.
|
|
603
651
|
*
|
|
604
652
|
* @example
|
|
605
653
|
* ```typescript
|
|
606
|
-
* MimeTypeSchema.parse('image/*'); // Valid
|
|
607
|
-
* MimeTypeSchema.parse('video/mp4'); // Valid
|
|
654
|
+
* MimeTypeSchema.parse('image/*'); // Valid - wildcard
|
|
655
|
+
* MimeTypeSchema.parse('video/mp4'); // Valid - standard
|
|
656
|
+
* MimeTypeSchema.parse('application/vnd.api+json'); // Valid - with dots/plus
|
|
608
657
|
* MimeTypeSchema.parse('invalid'); // Throws ZodError
|
|
609
658
|
* ```
|
|
610
659
|
*/
|
|
611
660
|
const MimeTypeSchema = zod.z
|
|
612
661
|
.string()
|
|
613
|
-
.regex(/^[a-
|
|
662
|
+
.regex(/^[a-z0-9]+\/[a-z0-9*+._-]+$/i, 'Invalid MIME type pattern. Expected format: type/subtype (e.g., "image/*", "video/mp4", "application/vnd.api+json")');
|
|
614
663
|
/**
|
|
615
664
|
* Zod schema for NSID (Namespaced Identifier).
|
|
616
665
|
*
|
|
617
666
|
* NSIDs are reverse-DNS style identifiers used throughout ATProto
|
|
618
667
|
* (e.g., "app.bsky.feed.post" or "com.example.myrecord").
|
|
619
668
|
*
|
|
669
|
+
* Official ATProto NSID spec requires:
|
|
670
|
+
* - Each segment must be 1-63 characters
|
|
671
|
+
* - Authority segments (all but last) can contain hyphens, but not at boundaries
|
|
672
|
+
* - Name segment (last) must start with a letter and contain only alphanumerics
|
|
673
|
+
* - Hyphens only allowed in authority segments, not in the name segment
|
|
674
|
+
*
|
|
620
675
|
* @see https://atproto.com/specs/nsid
|
|
621
676
|
*
|
|
622
677
|
* @example
|
|
@@ -628,7 +683,7 @@ const MimeTypeSchema = zod.z
|
|
|
628
683
|
*/
|
|
629
684
|
const NsidSchema = zod.z
|
|
630
685
|
.string()
|
|
631
|
-
.regex(/^[a-zA-Z][a-zA-Z0-9-]
|
|
686
|
+
.regex(/^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$/, 'Invalid NSID format. Expected reverse-DNS format (e.g., "app.bsky.feed.post")');
|
|
632
687
|
/**
|
|
633
688
|
* Zod schema for account permission.
|
|
634
689
|
*
|
|
@@ -1379,27 +1434,119 @@ const ScopePresets = {
|
|
|
1379
1434
|
function parseScope(scope) {
|
|
1380
1435
|
return scope.trim().split(/\s+/).filter(Boolean);
|
|
1381
1436
|
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Helper function to match MIME type patterns with wildcard support.
|
|
1439
|
+
*
|
|
1440
|
+
* Implements MIME type matching for blob permissions per ATProto spec.
|
|
1441
|
+
*
|
|
1442
|
+
* **Reference:**
|
|
1443
|
+
* - ATProto Permission Spec: https://atproto.com/specs/permission
|
|
1444
|
+
* Supports "MIME types or partial MIME type glob patterns"
|
|
1445
|
+
*
|
|
1446
|
+
* **Supported patterns:**
|
|
1447
|
+
* - Exact matches: "image/png" matches "image/png"
|
|
1448
|
+
* - Type wildcards: "image/*" matches "image/png", "image/jpeg", etc.
|
|
1449
|
+
* - Full wildcards: `*` `/` `*` matches any MIME type
|
|
1450
|
+
*
|
|
1451
|
+
* @param pattern - The MIME type pattern (may contain wildcards)
|
|
1452
|
+
* @param mimeType - The actual MIME type to check
|
|
1453
|
+
* @returns True if the MIME type matches the pattern
|
|
1454
|
+
*
|
|
1455
|
+
* @example
|
|
1456
|
+
* ```typescript
|
|
1457
|
+
* matchMimePattern("image/*", "image/png"); // true
|
|
1458
|
+
* matchMimePattern("*" + "/" + "*", "video/mp4"); // true - matches any MIME
|
|
1459
|
+
* matchMimePattern("image/*", "video/mp4"); // false
|
|
1460
|
+
* ```
|
|
1461
|
+
*/
|
|
1462
|
+
function matchMimePattern(pattern, mimeType) {
|
|
1463
|
+
if (pattern === "*/*")
|
|
1464
|
+
return true;
|
|
1465
|
+
if (pattern === mimeType)
|
|
1466
|
+
return true;
|
|
1467
|
+
const [patternType, patternSubtype] = pattern.split("/");
|
|
1468
|
+
const [mimeTypeType] = mimeType.split("/");
|
|
1469
|
+
if (patternSubtype === "*" && patternType === mimeTypeType) {
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1382
1474
|
/**
|
|
1383
1475
|
* Check if a scope string contains a specific permission.
|
|
1384
1476
|
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1477
|
+
* Implements permission matching per ATProto OAuth spec with support for
|
|
1478
|
+
* exact matching and limited wildcard patterns.
|
|
1479
|
+
*
|
|
1480
|
+
* **References:**
|
|
1481
|
+
* - ATProto Permission Spec: https://atproto.com/specs/permission
|
|
1482
|
+
* - ATProto OAuth Spec: https://atproto.com/specs/oauth
|
|
1483
|
+
*
|
|
1484
|
+
* **Supported wildcards (per spec):**
|
|
1485
|
+
* - `repo:*` - Matches any repository collection (e.g., `repo:app.bsky.feed.post`)
|
|
1486
|
+
* - `rpc:*` - Matches any RPC lexicon (but aud cannot also be wildcard)
|
|
1487
|
+
* - `blob:image/*` - MIME type wildcards (e.g., matches `blob:image/png`, `blob:image/jpeg`)
|
|
1488
|
+
* - `blob:` + wildcard MIME - Matches any MIME type (using `*` + `/` + `*` pattern)
|
|
1489
|
+
* - `identity:*` - Full control of DID document and handle (spec allows `*` as attr value)
|
|
1490
|
+
*
|
|
1491
|
+
* **NOT supported (per spec):**
|
|
1492
|
+
* - `account:*` - Account attr does not support wildcards (only `email` and `repo` allowed)
|
|
1493
|
+
* - `include:*` - Include NSID does not support wildcards
|
|
1494
|
+
* - Partial wildcards like `com.example.*` are not supported
|
|
1388
1495
|
*
|
|
1389
1496
|
* @param scope - Space-separated scope string
|
|
1390
1497
|
* @param permission - The permission to check for
|
|
1391
1498
|
* @returns True if the scope contains the permission
|
|
1392
1499
|
*
|
|
1393
|
-
* @example
|
|
1500
|
+
* @example Exact matching
|
|
1394
1501
|
* ```typescript
|
|
1395
|
-
* const scope = "account:email
|
|
1396
|
-
* hasPermission(scope, "account:email
|
|
1502
|
+
* const scope = "account:email repo:app.bsky.feed.post";
|
|
1503
|
+
* hasPermission(scope, "account:email"); // true
|
|
1397
1504
|
* hasPermission(scope, "account:repo"); // false
|
|
1398
1505
|
* ```
|
|
1506
|
+
*
|
|
1507
|
+
* @example Wildcard matching
|
|
1508
|
+
* ```typescript
|
|
1509
|
+
* const scope = "repo:* blob:image/* identity:*";
|
|
1510
|
+
* hasPermission(scope, "repo:app.bsky.feed.post"); // true
|
|
1511
|
+
* hasPermission(scope, "blob:image/png"); // true
|
|
1512
|
+
* hasPermission(scope, "blob:video/mp4"); // false
|
|
1513
|
+
* hasPermission(scope, "identity:handle"); // true
|
|
1514
|
+
* ```
|
|
1399
1515
|
*/
|
|
1400
1516
|
function hasPermission(scope, permission) {
|
|
1401
1517
|
const permissions = parseScope(scope);
|
|
1402
|
-
|
|
1518
|
+
// 1. Check exact match first
|
|
1519
|
+
if (permissions.includes(permission)) {
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
// 2. Check wildcard matches (only those supported by ATProto spec)
|
|
1523
|
+
for (const scopePermission of permissions) {
|
|
1524
|
+
// repo:* - Wildcard for repository collections
|
|
1525
|
+
// Spec: "Wildcard (*) is allowed in scope string syntax"
|
|
1526
|
+
if (scopePermission.startsWith("repo:*") && permission.startsWith("repo:")) {
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
// rpc:* - Wildcard for RPC lexicons
|
|
1530
|
+
// Spec: "Wildcard (*) is allowed in scope string syntax for lxm parameter"
|
|
1531
|
+
if (scopePermission.startsWith("rpc:*") && permission.startsWith("rpc:")) {
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
// blob MIME wildcards - image/*, video/*, or full wildcard
|
|
1535
|
+
// Spec: "MIME types or partial MIME type glob patterns"
|
|
1536
|
+
if (scopePermission.startsWith("blob:") && permission.startsWith("blob:")) {
|
|
1537
|
+
const scopeMime = scopePermission.substring(5).split("?")[0]; // Remove "blob:" prefix and query params
|
|
1538
|
+
const permMime = permission.substring(5).split("?")[0];
|
|
1539
|
+
if (matchMimePattern(scopeMime, permMime)) {
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
// identity:* - Full control of DID document and handle
|
|
1544
|
+
// Spec: "* - Full control of DID document and handle" (as attr value)
|
|
1545
|
+
if (scopePermission === "identity:*" && permission.startsWith("identity:")) {
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return false;
|
|
1403
1550
|
}
|
|
1404
1551
|
/**
|
|
1405
1552
|
* Check if a scope string contains all of the specified permissions.
|
|
@@ -1623,6 +1770,26 @@ class OAuthClient {
|
|
|
1623
1770
|
async getClient() {
|
|
1624
1771
|
return this.clientPromise;
|
|
1625
1772
|
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Detects if a URL is a loopback address (localhost or 127.0.0.1 or [::1]).
|
|
1775
|
+
*
|
|
1776
|
+
* @param urlString - The URL to check
|
|
1777
|
+
* @returns True if the URL is an HTTP loopback address
|
|
1778
|
+
* @internal
|
|
1779
|
+
*/
|
|
1780
|
+
isLoopbackUrl(urlString) {
|
|
1781
|
+
try {
|
|
1782
|
+
const url = new URL(urlString);
|
|
1783
|
+
if (url.protocol !== "http:") {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
const hostname = url.hostname.toLowerCase();
|
|
1787
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1626
1793
|
/**
|
|
1627
1794
|
* Builds OAuth client metadata from configuration.
|
|
1628
1795
|
*
|
|
@@ -1641,6 +1808,23 @@ class OAuthClient {
|
|
|
1641
1808
|
*/
|
|
1642
1809
|
buildClientMetadata() {
|
|
1643
1810
|
const clientIdUrl = new URL(this.config.oauth.clientId);
|
|
1811
|
+
// Detect and warn about loopback configuration
|
|
1812
|
+
const isDevelopment = isLoopbackUrl(this.config.oauth.clientId) || isLoopbackUrl(this.config.oauth.redirectUri);
|
|
1813
|
+
if (isDevelopment && !this.config.oauth.developmentMode) {
|
|
1814
|
+
this.logger?.warn("Using HTTP loopback URLs without explicit developmentMode flag", {
|
|
1815
|
+
clientId: this.config.oauth.clientId,
|
|
1816
|
+
redirectUri: this.config.oauth.redirectUri,
|
|
1817
|
+
note: "This is suitable for local development only. For production, use HTTPS URLs.",
|
|
1818
|
+
recommendation: "Set oauth.developmentMode: true to suppress this warning.",
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
if (isDevelopment) {
|
|
1822
|
+
this.logger?.info("Running in development mode with loopback URLs", {
|
|
1823
|
+
clientId: this.config.oauth.clientId,
|
|
1824
|
+
redirectUri: this.config.oauth.redirectUri,
|
|
1825
|
+
note: "Authorization server must support loopback clients (optional per AT Protocol spec)",
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1644
1828
|
const metadata = {
|
|
1645
1829
|
client_id: this.config.oauth.clientId,
|
|
1646
1830
|
client_name: "ATProto SDK Client",
|
|
@@ -2078,6 +2262,299 @@ class ConfigurableAgent extends api.Agent {
|
|
|
2078
2262
|
}
|
|
2079
2263
|
}
|
|
2080
2264
|
|
|
2265
|
+
/**
|
|
2266
|
+
* LexiconRegistry - Manages custom lexicon registration and validation.
|
|
2267
|
+
*
|
|
2268
|
+
* This module provides a registry for AT Protocol lexicon schemas,
|
|
2269
|
+
* allowing developers to register custom lexicons and validate records
|
|
2270
|
+
* against registered schemas.
|
|
2271
|
+
*
|
|
2272
|
+
* @packageDocumentation
|
|
2273
|
+
*/
|
|
2274
|
+
/**
|
|
2275
|
+
* Registry for managing AT Protocol lexicon schemas.
|
|
2276
|
+
*
|
|
2277
|
+
* The LexiconRegistry allows developers to:
|
|
2278
|
+
* - Register custom lexicon definitions
|
|
2279
|
+
* - Validate records against registered schemas
|
|
2280
|
+
* - Query registered lexicons
|
|
2281
|
+
* - Add lexicons to AT Protocol agents
|
|
2282
|
+
*
|
|
2283
|
+
* @example Basic usage
|
|
2284
|
+
* ```typescript
|
|
2285
|
+
* const registry = new LexiconRegistry();
|
|
2286
|
+
*
|
|
2287
|
+
* // Register a custom lexicon
|
|
2288
|
+
* registry.register({
|
|
2289
|
+
* lexicon: 1,
|
|
2290
|
+
* id: "org.myapp.customRecord",
|
|
2291
|
+
* defs: {
|
|
2292
|
+
* main: {
|
|
2293
|
+
* type: "record",
|
|
2294
|
+
* key: "tid",
|
|
2295
|
+
* record: {
|
|
2296
|
+
* type: "object",
|
|
2297
|
+
* required: ["$type", "title"],
|
|
2298
|
+
* properties: {
|
|
2299
|
+
* "$type": { type: "string", const: "org.myapp.customRecord" },
|
|
2300
|
+
* title: { type: "string" }
|
|
2301
|
+
* }
|
|
2302
|
+
* }
|
|
2303
|
+
* }
|
|
2304
|
+
* }
|
|
2305
|
+
* });
|
|
2306
|
+
*
|
|
2307
|
+
* // Validate a record
|
|
2308
|
+
* const result = registry.validate("org.myapp.customRecord", {
|
|
2309
|
+
* $type: "org.myapp.customRecord",
|
|
2310
|
+
* title: "My Record"
|
|
2311
|
+
* });
|
|
2312
|
+
*
|
|
2313
|
+
* if (!result.valid) {
|
|
2314
|
+
* console.error(result.error);
|
|
2315
|
+
* }
|
|
2316
|
+
* ```
|
|
2317
|
+
*/
|
|
2318
|
+
class LexiconRegistry {
|
|
2319
|
+
/**
|
|
2320
|
+
* Creates a new LexiconRegistry instance.
|
|
2321
|
+
*
|
|
2322
|
+
* @param initialLexicons - Optional array of lexicons to register on initialization
|
|
2323
|
+
*/
|
|
2324
|
+
constructor(initialLexicons) {
|
|
2325
|
+
this.lexicons = new lexicon$1.Lexicons();
|
|
2326
|
+
this.registeredIds = new Set();
|
|
2327
|
+
if (initialLexicons && initialLexicons.length > 0) {
|
|
2328
|
+
this.registerMany(initialLexicons);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Registers a single lexicon definition.
|
|
2333
|
+
*
|
|
2334
|
+
* @param lexicon - The lexicon document to register
|
|
2335
|
+
* @throws {Error} If the lexicon is invalid or already registered
|
|
2336
|
+
*
|
|
2337
|
+
* @example
|
|
2338
|
+
* ```typescript
|
|
2339
|
+
* registry.register({
|
|
2340
|
+
* lexicon: 1,
|
|
2341
|
+
* id: "org.myapp.customRecord",
|
|
2342
|
+
* defs: { ... }
|
|
2343
|
+
* });
|
|
2344
|
+
* ```
|
|
2345
|
+
*/
|
|
2346
|
+
register(lexicon) {
|
|
2347
|
+
if (!lexicon.id) {
|
|
2348
|
+
throw new Error("Lexicon must have an id");
|
|
2349
|
+
}
|
|
2350
|
+
if (this.registeredIds.has(lexicon.id)) {
|
|
2351
|
+
throw new Error(`Lexicon ${lexicon.id} is already registered`);
|
|
2352
|
+
}
|
|
2353
|
+
try {
|
|
2354
|
+
// Check if the lexicon already exists in the internal store
|
|
2355
|
+
// (e.g., after unregister which only removes from registeredIds)
|
|
2356
|
+
const existingLexicon = this.lexicons.get(lexicon.id);
|
|
2357
|
+
if (!existingLexicon) {
|
|
2358
|
+
// Lexicon is truly new, add it to the store
|
|
2359
|
+
this.lexicons.add(lexicon);
|
|
2360
|
+
}
|
|
2361
|
+
// Always add to registeredIds (re-enable if previously unregistered)
|
|
2362
|
+
this.registeredIds.add(lexicon.id);
|
|
2363
|
+
}
|
|
2364
|
+
catch (error) {
|
|
2365
|
+
throw new Error(`Failed to register lexicon ${lexicon.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Registers multiple lexicon definitions at once.
|
|
2370
|
+
*
|
|
2371
|
+
* @param lexicons - Array of lexicon documents to register
|
|
2372
|
+
* @throws {Error} If any lexicon is invalid or already registered
|
|
2373
|
+
*
|
|
2374
|
+
* @example
|
|
2375
|
+
* ```typescript
|
|
2376
|
+
* registry.registerMany([lexicon1, lexicon2, lexicon3]);
|
|
2377
|
+
* ```
|
|
2378
|
+
*/
|
|
2379
|
+
registerMany(lexicons) {
|
|
2380
|
+
for (const lexicon of lexicons) {
|
|
2381
|
+
this.register(lexicon);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Registers a lexicon from a JSON object.
|
|
2386
|
+
*
|
|
2387
|
+
* This is a convenience method for registering lexicons loaded from JSON files.
|
|
2388
|
+
*
|
|
2389
|
+
* @param lexiconJson - The lexicon as a plain JavaScript object
|
|
2390
|
+
* @throws {ValidationError} If the lexicon is not a valid object
|
|
2391
|
+
* @throws {Error} If the lexicon is invalid or already registered
|
|
2392
|
+
*
|
|
2393
|
+
* @example
|
|
2394
|
+
* ```typescript
|
|
2395
|
+
* import customLexicon from "./custom-lexicon.json";
|
|
2396
|
+
* registry.registerFromJSON(customLexicon);
|
|
2397
|
+
* ```
|
|
2398
|
+
*/
|
|
2399
|
+
registerFromJSON(lexiconJson) {
|
|
2400
|
+
// Validate that input is an object and not null
|
|
2401
|
+
if (typeof lexiconJson !== "object" || lexiconJson === null) {
|
|
2402
|
+
throw new ValidationError("Lexicon JSON must be a valid object");
|
|
2403
|
+
}
|
|
2404
|
+
// Now we can safely cast to LexiconDoc and register
|
|
2405
|
+
this.register(lexiconJson);
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Unregisters a lexicon by its NSID.
|
|
2409
|
+
*
|
|
2410
|
+
* @param nsid - The NSID of the lexicon to unregister
|
|
2411
|
+
* @returns True if the lexicon was unregistered, false if it wasn't registered
|
|
2412
|
+
*
|
|
2413
|
+
* @example
|
|
2414
|
+
* ```typescript
|
|
2415
|
+
* registry.unregister("org.myapp.customRecord");
|
|
2416
|
+
* ```
|
|
2417
|
+
*/
|
|
2418
|
+
unregister(nsid) {
|
|
2419
|
+
if (!this.registeredIds.has(nsid)) {
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
this.registeredIds.delete(nsid);
|
|
2423
|
+
// Note: Lexicons class doesn't have a remove method,
|
|
2424
|
+
// so we can't actually remove from the internal store.
|
|
2425
|
+
// We track removal in our Set for isRegistered checks.
|
|
2426
|
+
return true;
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Checks if a lexicon is registered.
|
|
2430
|
+
*
|
|
2431
|
+
* @param nsid - The NSID to check
|
|
2432
|
+
* @returns True if the lexicon is registered
|
|
2433
|
+
*
|
|
2434
|
+
* @example
|
|
2435
|
+
* ```typescript
|
|
2436
|
+
* if (registry.isRegistered("org.myapp.customRecord")) {
|
|
2437
|
+
* // Lexicon is available
|
|
2438
|
+
* }
|
|
2439
|
+
* ```
|
|
2440
|
+
*/
|
|
2441
|
+
isRegistered(nsid) {
|
|
2442
|
+
return this.registeredIds.has(nsid);
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Gets a lexicon definition by its NSID.
|
|
2446
|
+
*
|
|
2447
|
+
* @param nsid - The NSID of the lexicon to retrieve
|
|
2448
|
+
* @returns The lexicon document, or undefined if not found
|
|
2449
|
+
*
|
|
2450
|
+
* @example
|
|
2451
|
+
* ```typescript
|
|
2452
|
+
* const lexicon = registry.get("org.myapp.customRecord");
|
|
2453
|
+
* if (lexicon) {
|
|
2454
|
+
* console.log(lexicon.defs);
|
|
2455
|
+
* }
|
|
2456
|
+
* ```
|
|
2457
|
+
*/
|
|
2458
|
+
get(nsid) {
|
|
2459
|
+
if (!this.isRegistered(nsid)) {
|
|
2460
|
+
return undefined;
|
|
2461
|
+
}
|
|
2462
|
+
return this.lexicons.get(nsid);
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Gets all registered lexicon NSIDs.
|
|
2466
|
+
*
|
|
2467
|
+
* @returns Array of registered NSIDs
|
|
2468
|
+
*
|
|
2469
|
+
* @example
|
|
2470
|
+
* ```typescript
|
|
2471
|
+
* const registered = registry.getAll();
|
|
2472
|
+
* console.log(`Registered lexicons: ${registered.join(", ")}`);
|
|
2473
|
+
* ```
|
|
2474
|
+
*/
|
|
2475
|
+
getAll() {
|
|
2476
|
+
return Array.from(this.registeredIds);
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Validates a record against a registered lexicon.
|
|
2480
|
+
*
|
|
2481
|
+
* @param nsid - The collection NSID to validate against
|
|
2482
|
+
* @param record - The record data to validate
|
|
2483
|
+
* @returns Validation result with success status and optional error message
|
|
2484
|
+
*
|
|
2485
|
+
* @example
|
|
2486
|
+
* ```typescript
|
|
2487
|
+
* const result = registry.validate("org.myapp.customRecord", {
|
|
2488
|
+
* $type: "org.myapp.customRecord",
|
|
2489
|
+
* title: "My Record"
|
|
2490
|
+
* });
|
|
2491
|
+
*
|
|
2492
|
+
* if (!result.valid) {
|
|
2493
|
+
* console.error(`Validation failed: ${result.error}`);
|
|
2494
|
+
* }
|
|
2495
|
+
* ```
|
|
2496
|
+
*/
|
|
2497
|
+
validate(nsid, record) {
|
|
2498
|
+
if (!this.isRegistered(nsid)) {
|
|
2499
|
+
return {
|
|
2500
|
+
valid: false,
|
|
2501
|
+
error: `Lexicon ${nsid} is not registered`,
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
try {
|
|
2505
|
+
this.lexicons.assertValidRecord(nsid, record);
|
|
2506
|
+
return { valid: true };
|
|
2507
|
+
}
|
|
2508
|
+
catch (error) {
|
|
2509
|
+
return {
|
|
2510
|
+
valid: false,
|
|
2511
|
+
error: error instanceof Error ? error.message : "Validation failed",
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Adds all registered lexicons to an AT Protocol Agent.
|
|
2517
|
+
*
|
|
2518
|
+
* This method is currently a no-op as the AT Protocol Agent
|
|
2519
|
+
* doesn't provide a public API for adding lexicons at runtime.
|
|
2520
|
+
* Lexicons must be registered with the server.
|
|
2521
|
+
*
|
|
2522
|
+
* This method is kept for future compatibility if the API
|
|
2523
|
+
* adds support for client-side lexicon registration.
|
|
2524
|
+
*
|
|
2525
|
+
* @param _agent - The AT Protocol Agent (currently unused)
|
|
2526
|
+
*
|
|
2527
|
+
* @example
|
|
2528
|
+
* ```typescript
|
|
2529
|
+
* const agent = new Agent(session);
|
|
2530
|
+
* registry.addToAgent(agent);
|
|
2531
|
+
* // Reserved for future use
|
|
2532
|
+
* ```
|
|
2533
|
+
*/
|
|
2534
|
+
addToAgent(_agent) {
|
|
2535
|
+
// No-op: AT Protocol Agent doesn't support client-side lexicon addition
|
|
2536
|
+
// Lexicons are validated client-side via this registry,
|
|
2537
|
+
// but server-side validation is performed by the PDS/SDS
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Gets the underlying Lexicons instance.
|
|
2541
|
+
*
|
|
2542
|
+
* This provides direct access to the AT Protocol Lexicons object
|
|
2543
|
+
* for advanced use cases.
|
|
2544
|
+
*
|
|
2545
|
+
* @returns The internal Lexicons instance
|
|
2546
|
+
*
|
|
2547
|
+
* @example
|
|
2548
|
+
* ```typescript
|
|
2549
|
+
* const lexicons = registry.getLexicons();
|
|
2550
|
+
* // Use lexicons directly for advanced operations
|
|
2551
|
+
* ```
|
|
2552
|
+
*/
|
|
2553
|
+
getLexicons() {
|
|
2554
|
+
return this.lexicons;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2081
2558
|
/**
|
|
2082
2559
|
* RecordOperationsImpl - Low-level record CRUD operations.
|
|
2083
2560
|
*
|
|
@@ -2132,12 +2609,14 @@ class RecordOperationsImpl {
|
|
|
2132
2609
|
*
|
|
2133
2610
|
* @param agent - AT Protocol Agent for making API calls
|
|
2134
2611
|
* @param repoDid - DID of the repository to operate on
|
|
2612
|
+
* @param lexiconRegistry - Optional registry for validating records against lexicon schemas
|
|
2135
2613
|
*
|
|
2136
2614
|
* @internal
|
|
2137
2615
|
*/
|
|
2138
|
-
constructor(agent, repoDid) {
|
|
2616
|
+
constructor(agent, repoDid, lexiconRegistry) {
|
|
2139
2617
|
this.agent = agent;
|
|
2140
2618
|
this.repoDid = repoDid;
|
|
2619
|
+
this.lexiconRegistry = lexiconRegistry;
|
|
2141
2620
|
}
|
|
2142
2621
|
/**
|
|
2143
2622
|
* Creates a new record in the specified collection.
|
|
@@ -2147,14 +2626,17 @@ class RecordOperationsImpl {
|
|
|
2147
2626
|
* @param params.record - Record data conforming to the collection's lexicon schema
|
|
2148
2627
|
* @param params.rkey - Optional record key. If not provided, a TID (timestamp-based ID)
|
|
2149
2628
|
* is automatically generated by the server.
|
|
2629
|
+
* @param params.skipValidation - Optional flag to skip lexicon validation (default: false)
|
|
2150
2630
|
* @returns Promise resolving to the created record's URI and CID
|
|
2151
2631
|
* @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
|
|
2152
2632
|
* @throws {@link NetworkError} if the API request fails
|
|
2153
2633
|
*
|
|
2154
2634
|
* @remarks
|
|
2155
2635
|
* The record is validated against the collection's lexicon before sending
|
|
2156
|
-
* to the server
|
|
2157
|
-
* is
|
|
2636
|
+
* to the server if the collection is registered in the LexiconRegistry.
|
|
2637
|
+
* If no lexicon is registered for the collection, validation is skipped
|
|
2638
|
+
* (allowing custom record types). Use `skipValidation: true` to bypass
|
|
2639
|
+
* validation even for registered collections.
|
|
2158
2640
|
*
|
|
2159
2641
|
* **AT-URI Format**: `at://{did}/{collection}/{rkey}`
|
|
2160
2642
|
*
|
|
@@ -2174,6 +2656,13 @@ class RecordOperationsImpl {
|
|
|
2174
2656
|
*/
|
|
2175
2657
|
async create(params) {
|
|
2176
2658
|
try {
|
|
2659
|
+
// Validate record against registered lexicon if available
|
|
2660
|
+
if (!params.skipValidation && this.lexiconRegistry?.isRegistered(params.collection)) {
|
|
2661
|
+
const validation = this.lexiconRegistry.validate(params.collection, params.record);
|
|
2662
|
+
if (!validation.valid) {
|
|
2663
|
+
throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2177
2666
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
2178
2667
|
repo: this.repoDid,
|
|
2179
2668
|
collection: params.collection,
|
|
@@ -2198,6 +2687,7 @@ class RecordOperationsImpl {
|
|
|
2198
2687
|
* @param params.collection - NSID of the collection
|
|
2199
2688
|
* @param params.rkey - Record key (the last segment of the AT-URI)
|
|
2200
2689
|
* @param params.record - New record data (completely replaces existing record)
|
|
2690
|
+
* @param params.skipValidation - Optional flag to skip lexicon validation (default: false)
|
|
2201
2691
|
* @returns Promise resolving to the updated record's URI and new CID
|
|
2202
2692
|
* @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
|
|
2203
2693
|
* @throws {@link NetworkError} if the API request fails
|
|
@@ -2207,6 +2697,10 @@ class RecordOperationsImpl {
|
|
|
2207
2697
|
* record is replaced with the new data. To preserve existing fields,
|
|
2208
2698
|
* first fetch the record with {@link get}, modify it, then update.
|
|
2209
2699
|
*
|
|
2700
|
+
* The record is validated against the collection's lexicon before sending
|
|
2701
|
+
* to the server if the collection is registered in the LexiconRegistry.
|
|
2702
|
+
* Use `skipValidation: true` to bypass validation.
|
|
2703
|
+
*
|
|
2210
2704
|
* @example
|
|
2211
2705
|
* ```typescript
|
|
2212
2706
|
* // Get existing record
|
|
@@ -2228,6 +2722,13 @@ class RecordOperationsImpl {
|
|
|
2228
2722
|
*/
|
|
2229
2723
|
async update(params) {
|
|
2230
2724
|
try {
|
|
2725
|
+
// Validate record against registered lexicon if available
|
|
2726
|
+
if (!params.skipValidation && this.lexiconRegistry?.isRegistered(params.collection)) {
|
|
2727
|
+
const validation = this.lexiconRegistry.validate(params.collection, params.record);
|
|
2728
|
+
if (!validation.valid) {
|
|
2729
|
+
throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2231
2732
|
const result = await this.agent.com.atproto.repo.putRecord({
|
|
2232
2733
|
repo: this.repoDid,
|
|
2233
2734
|
collection: params.collection,
|
|
@@ -2884,20 +3385,21 @@ class ProfileOperationsImpl {
|
|
|
2884
3385
|
const HYPERCERT_LEXICONS = [
|
|
2885
3386
|
lexicon.CERTIFIED_DEFS_LEXICON_JSON,
|
|
2886
3387
|
lexicon.LOCATION_LEXICON_JSON,
|
|
2887
|
-
lexicon.
|
|
3388
|
+
lexicon.STRONG_REF_LEXICON_JSON,
|
|
2888
3389
|
lexicon.HYPERCERTS_DEFS_LEXICON_JSON,
|
|
2889
3390
|
lexicon.ACTIVITY_LEXICON_JSON,
|
|
2890
3391
|
lexicon.COLLECTION_LEXICON_JSON,
|
|
2891
|
-
lexicon.
|
|
3392
|
+
lexicon.CONTRIBUTION_DETAILS_LEXICON_JSON,
|
|
3393
|
+
lexicon.CONTRIBUTOR_INFORMATION_LEXICON_JSON,
|
|
2892
3394
|
lexicon.EVALUATION_LEXICON_JSON,
|
|
2893
3395
|
lexicon.EVIDENCE_LEXICON_JSON,
|
|
2894
3396
|
lexicon.MEASUREMENT_LEXICON_JSON,
|
|
2895
3397
|
lexicon.RIGHTS_LEXICON_JSON,
|
|
2896
|
-
lexicon.PROJECT_LEXICON_JSON,
|
|
2897
3398
|
lexicon.BADGE_AWARD_LEXICON_JSON,
|
|
2898
3399
|
lexicon.BADGE_DEFINITION_LEXICON_JSON,
|
|
2899
3400
|
lexicon.BADGE_RESPONSE_LEXICON_JSON,
|
|
2900
3401
|
lexicon.FUNDING_RECEIPT_LEXICON_JSON,
|
|
3402
|
+
lexicon.WORK_SCOPE_TAG_LEXICON_JSON,
|
|
2901
3403
|
];
|
|
2902
3404
|
/**
|
|
2903
3405
|
* Collection NSIDs (Namespaced Identifiers) for hypercert records.
|
|
@@ -2919,9 +3421,15 @@ const HYPERCERT_COLLECTIONS = {
|
|
|
2919
3421
|
*/
|
|
2920
3422
|
LOCATION: lexicon.LOCATION_NSID,
|
|
2921
3423
|
/**
|
|
2922
|
-
* Contribution record collection.
|
|
3424
|
+
* Contribution details record collection.
|
|
3425
|
+
* For storing details about a specific contribution (role, description, timeframe).
|
|
2923
3426
|
*/
|
|
2924
|
-
|
|
3427
|
+
CONTRIBUTION_DETAILS: lexicon.CONTRIBUTION_DETAILS_NSID,
|
|
3428
|
+
/**
|
|
3429
|
+
* Contributor information record collection.
|
|
3430
|
+
* For storing contributor profile information (identifier, displayName, image).
|
|
3431
|
+
*/
|
|
3432
|
+
CONTRIBUTOR_INFORMATION: lexicon.CONTRIBUTOR_INFORMATION_NSID,
|
|
2925
3433
|
/**
|
|
2926
3434
|
* Measurement record collection.
|
|
2927
3435
|
*/
|
|
@@ -2936,12 +3444,9 @@ const HYPERCERT_COLLECTIONS = {
|
|
|
2936
3444
|
EVIDENCE: lexicon.EVIDENCE_NSID,
|
|
2937
3445
|
/**
|
|
2938
3446
|
* Collection record collection (groups of hypercerts).
|
|
3447
|
+
* Projects are now collections with type='project'.
|
|
2939
3448
|
*/
|
|
2940
3449
|
COLLECTION: lexicon.COLLECTION_NSID,
|
|
2941
|
-
/**
|
|
2942
|
-
* Project record collection.
|
|
2943
|
-
*/
|
|
2944
|
-
PROJECT: lexicon.PROJECT_NSID,
|
|
2945
3450
|
/**
|
|
2946
3451
|
* Badge award record collection.
|
|
2947
3452
|
*/
|
|
@@ -2958,6 +3463,11 @@ const HYPERCERT_COLLECTIONS = {
|
|
|
2958
3463
|
* Funding receipt record collection.
|
|
2959
3464
|
*/
|
|
2960
3465
|
FUNDING_RECEIPT: lexicon.FUNDING_RECEIPT_NSID,
|
|
3466
|
+
/**
|
|
3467
|
+
* Work scope tag record collection.
|
|
3468
|
+
* For defining reusable work scope atoms.
|
|
3469
|
+
*/
|
|
3470
|
+
WORK_SCOPE_TAG: lexicon.WORK_SCOPE_TAG_NSID,
|
|
2961
3471
|
};
|
|
2962
3472
|
|
|
2963
3473
|
/**
|
|
@@ -3051,6 +3561,26 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3051
3561
|
}
|
|
3052
3562
|
}
|
|
3053
3563
|
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Helper function to upload a blob to the repository, returns a blob reference
|
|
3566
|
+
*
|
|
3567
|
+
* @param content - Blob to upload
|
|
3568
|
+
* @param fallbackContentType | if content.type is empty,we use this
|
|
3569
|
+
* @returns BlobRef
|
|
3570
|
+
* @throws {@link NetworkError} if upload fails
|
|
3571
|
+
* @internal
|
|
3572
|
+
*/
|
|
3573
|
+
async handleBlobUpload(content, fallbackContentType) {
|
|
3574
|
+
const arrayBuffer = await content.arrayBuffer();
|
|
3575
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
3576
|
+
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
3577
|
+
encoding: content.type || fallbackContentType,
|
|
3578
|
+
});
|
|
3579
|
+
if (!uploadResult.success) {
|
|
3580
|
+
throw new NetworkError("Failed to upload blob");
|
|
3581
|
+
}
|
|
3582
|
+
return uploadResult.data.blob;
|
|
3583
|
+
}
|
|
3054
3584
|
/**
|
|
3055
3585
|
* Uploads an image blob and returns a blob reference.
|
|
3056
3586
|
*
|
|
@@ -3161,9 +3691,6 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3161
3691
|
if (imageBlobRef) {
|
|
3162
3692
|
hypercertRecord.image = imageBlobRef;
|
|
3163
3693
|
}
|
|
3164
|
-
if (params.evidence && params.evidence.length > 0) {
|
|
3165
|
-
hypercertRecord.evidence = params.evidence;
|
|
3166
|
-
}
|
|
3167
3694
|
const hypercertValidation = lexicon.validate(hypercertRecord, HYPERCERT_COLLECTIONS.CLAIM, "main", false);
|
|
3168
3695
|
if (!hypercertValidation.success) {
|
|
3169
3696
|
throw new ValidationError(`Invalid hypercert record: ${hypercertValidation.error?.message}`);
|
|
@@ -3247,6 +3774,35 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3247
3774
|
throw error;
|
|
3248
3775
|
}
|
|
3249
3776
|
}
|
|
3777
|
+
/**
|
|
3778
|
+
* Creates evidence records with progress tracking.
|
|
3779
|
+
*
|
|
3780
|
+
* @param hypercertUri - URI of the hypercert
|
|
3781
|
+
* @param evidenceItems - Array of evidence data (without subjectUri)
|
|
3782
|
+
* @param onProgress - Optional progress callback
|
|
3783
|
+
* @returns Promise resolving to array of evidence URIs
|
|
3784
|
+
* @internal
|
|
3785
|
+
*/
|
|
3786
|
+
async createEvidenceWithProgress(hypercertUri, evidenceItems, onProgress) {
|
|
3787
|
+
this.emitProgress(onProgress, { name: "addEvidence", status: "start" });
|
|
3788
|
+
try {
|
|
3789
|
+
const evidenceUris = await Promise.all(evidenceItems.map((evidence) => this.addEvidence({
|
|
3790
|
+
subjectUri: hypercertUri,
|
|
3791
|
+
...evidence,
|
|
3792
|
+
}).then((result) => result.uri)));
|
|
3793
|
+
this.emitProgress(onProgress, {
|
|
3794
|
+
name: "addEvidence",
|
|
3795
|
+
status: "success",
|
|
3796
|
+
data: { count: evidenceUris.length },
|
|
3797
|
+
});
|
|
3798
|
+
return evidenceUris;
|
|
3799
|
+
}
|
|
3800
|
+
catch (error) {
|
|
3801
|
+
this.emitProgress(onProgress, { name: "addEvidence", status: "error", error: error });
|
|
3802
|
+
this.logger?.warn(`Failed to create evidence: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
3803
|
+
throw error;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3250
3806
|
/**
|
|
3251
3807
|
* Creates a new hypercert with all related records.
|
|
3252
3808
|
*
|
|
@@ -3275,6 +3831,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3275
3831
|
* - `createHypercert`: Main hypercert record creation
|
|
3276
3832
|
* - `attachLocation`: Location record creation
|
|
3277
3833
|
* - `createContributions`: Contribution records creation
|
|
3834
|
+
* - `addEvidence`: Evidence records creation
|
|
3278
3835
|
*
|
|
3279
3836
|
* @example Minimal hypercert
|
|
3280
3837
|
* ```typescript
|
|
@@ -3350,6 +3907,15 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3350
3907
|
// Error already logged and progress emitted
|
|
3351
3908
|
}
|
|
3352
3909
|
}
|
|
3910
|
+
// Step 6: Add evidence records if provided
|
|
3911
|
+
if (params.evidence && params.evidence.length > 0) {
|
|
3912
|
+
try {
|
|
3913
|
+
result.evidenceUris = await this.createEvidenceWithProgress(hypercertUri, params.evidence, params.onProgress);
|
|
3914
|
+
}
|
|
3915
|
+
catch {
|
|
3916
|
+
// Error already logged and progress emitted
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3353
3919
|
return result;
|
|
3354
3920
|
}
|
|
3355
3921
|
catch (error) {
|
|
@@ -3632,69 +4198,25 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3632
4198
|
*/
|
|
3633
4199
|
async attachLocation(hypercertUri, location) {
|
|
3634
4200
|
try {
|
|
3635
|
-
// Validate required srs field
|
|
3636
|
-
if (!location.srs) {
|
|
3637
|
-
throw new ValidationError("srs (Spatial Reference System) is required. Example: 'EPSG:4326' for WGS84 coordinates, or 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' for CRS84.");
|
|
3638
|
-
}
|
|
3639
4201
|
// Validate that hypercert exists (unused but confirms hypercert is valid)
|
|
3640
4202
|
await this.get(hypercertUri);
|
|
3641
|
-
const
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
});
|
|
3652
|
-
if (uploadResult.success) {
|
|
3653
|
-
locationData = {
|
|
3654
|
-
$type: "blob",
|
|
3655
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
3656
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
3657
|
-
size: uploadResult.data.blob.size,
|
|
3658
|
-
};
|
|
3659
|
-
locationType = "geojson-point";
|
|
3660
|
-
}
|
|
3661
|
-
else {
|
|
3662
|
-
throw new NetworkError("Failed to upload GeoJSON blob");
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
else {
|
|
3666
|
-
// Use value as a URI reference
|
|
3667
|
-
locationData = {
|
|
3668
|
-
$type: "org.hypercerts.defs#uri",
|
|
3669
|
-
uri: location.value,
|
|
3670
|
-
};
|
|
3671
|
-
locationType = "coordinate-decimal";
|
|
3672
|
-
}
|
|
3673
|
-
// Build location record according to app.certified.location lexicon
|
|
3674
|
-
const locationRecord = {
|
|
3675
|
-
$type: HYPERCERT_COLLECTIONS.LOCATION,
|
|
3676
|
-
lpVersion: "1.0",
|
|
3677
|
-
srs: location.srs,
|
|
3678
|
-
locationType,
|
|
3679
|
-
location: locationData,
|
|
3680
|
-
createdAt,
|
|
3681
|
-
name: location.name,
|
|
3682
|
-
description: location.description,
|
|
3683
|
-
};
|
|
3684
|
-
const validation = lexicon.validate(locationRecord, HYPERCERT_COLLECTIONS.LOCATION, "main", false);
|
|
3685
|
-
if (!validation.success) {
|
|
3686
|
-
throw new ValidationError(`Invalid location record: ${validation.error?.message}`);
|
|
3687
|
-
}
|
|
3688
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
3689
|
-
repo: this.repoDid,
|
|
3690
|
-
collection: HYPERCERT_COLLECTIONS.LOCATION,
|
|
3691
|
-
record: locationRecord,
|
|
4203
|
+
const resolvedLocation = await this.resolveLocation(location);
|
|
4204
|
+
await this.update({
|
|
4205
|
+
uri: hypercertUri,
|
|
4206
|
+
updates: {
|
|
4207
|
+
location: {
|
|
4208
|
+
$type: "com.atproto.repo.strongRef",
|
|
4209
|
+
uri: resolvedLocation.uri,
|
|
4210
|
+
cid: resolvedLocation.cid,
|
|
4211
|
+
},
|
|
4212
|
+
},
|
|
3692
4213
|
});
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
4214
|
+
this.emit("locationAttached", {
|
|
4215
|
+
uri: resolvedLocation.uri,
|
|
4216
|
+
cid: resolvedLocation.cid,
|
|
4217
|
+
hypercertUri,
|
|
4218
|
+
});
|
|
4219
|
+
return { uri: resolvedLocation.uri, cid: resolvedLocation.cid };
|
|
3698
4220
|
}
|
|
3699
4221
|
catch (error) {
|
|
3700
4222
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -3703,36 +4225,138 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3703
4225
|
}
|
|
3704
4226
|
}
|
|
3705
4227
|
/**
|
|
3706
|
-
*
|
|
4228
|
+
* Generic helper to resolve string | Blob into a URI or blob reference.
|
|
4229
|
+
*
|
|
4230
|
+
* @param content - Either a URI string or a Blob to upload
|
|
4231
|
+
* @param fallbackMimeType - MIME type to use if Blob.type is empty
|
|
4232
|
+
* @returns Promise resolving to either a URI ref or blob ref
|
|
4233
|
+
* @internal
|
|
4234
|
+
*/
|
|
4235
|
+
async resolveUriOrBlob(content, fallbackMimeType) {
|
|
4236
|
+
if (typeof content === "string") {
|
|
4237
|
+
const uriRef = {
|
|
4238
|
+
$type: "org.hypercerts.defs#uri",
|
|
4239
|
+
uri: content,
|
|
4240
|
+
};
|
|
4241
|
+
return uriRef;
|
|
4242
|
+
}
|
|
4243
|
+
const uploadedBlob = await this.handleBlobUpload(content, fallbackMimeType);
|
|
4244
|
+
const blobRef = {
|
|
4245
|
+
$type: "org.hypercerts.defs#smallBlob",
|
|
4246
|
+
blob: uploadedBlob,
|
|
4247
|
+
};
|
|
4248
|
+
return blobRef;
|
|
4249
|
+
}
|
|
4250
|
+
async resolveCollectionImageInput(input, isBanner = false) {
|
|
4251
|
+
if (typeof input === "string") {
|
|
4252
|
+
return { $type: "org.hypercerts.defs#uri", uri: input };
|
|
4253
|
+
}
|
|
4254
|
+
const blob = await this.handleBlobUpload(input, "image/jpeg");
|
|
4255
|
+
if (isBanner) {
|
|
4256
|
+
return { $type: "org.hypercerts.defs#largeImage", image: blob };
|
|
4257
|
+
}
|
|
4258
|
+
return { $type: "org.hypercerts.defs#smallImage", image: blob };
|
|
4259
|
+
}
|
|
4260
|
+
async resolveLocationValue(location) {
|
|
4261
|
+
if (typeof location === "string" || location instanceof Blob) {
|
|
4262
|
+
return this.resolveUriOrBlob(location, "application/geo+json");
|
|
4263
|
+
}
|
|
4264
|
+
return location;
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* Check if an AttachLocationParams is the object form (not a StrongRef or string).
|
|
4268
|
+
* @internal
|
|
4269
|
+
*/
|
|
4270
|
+
isLocationObject(location) {
|
|
4271
|
+
return (typeof location === "object" &&
|
|
4272
|
+
!("uri" in location) &&
|
|
4273
|
+
!("cid" in location) &&
|
|
4274
|
+
location !== null &&
|
|
4275
|
+
!Array.isArray(location));
|
|
4276
|
+
}
|
|
4277
|
+
/**
|
|
4278
|
+
* Helper to resolve a location reference to a StrongRef.
|
|
4279
|
+
*
|
|
4280
|
+
* @param location - Location parameter (StrongRef, string URI, or location object)
|
|
4281
|
+
* @returns Promise resolving to a StrongRef
|
|
4282
|
+
* @throws {ValidationError} When string input doesn't match AT-URI pattern
|
|
4283
|
+
* @throws {NetworkError} When getRecord fails or returns no CID
|
|
4284
|
+
* @internal
|
|
4285
|
+
*/
|
|
4286
|
+
async resolveLocation(location) {
|
|
4287
|
+
if (typeof location === "string") {
|
|
4288
|
+
return this.resolveStrongRefFromUri(location);
|
|
4289
|
+
}
|
|
4290
|
+
if (this.isLocationObject(location)) {
|
|
4291
|
+
return this.createLocationRecord(location);
|
|
4292
|
+
}
|
|
4293
|
+
if ("uri" in location && "cid" in location) {
|
|
4294
|
+
return { $type: "com.atproto.repo.strongRef", uri: location.uri, cid: location.cid };
|
|
4295
|
+
}
|
|
4296
|
+
throw new ValidationError("resolveLocation: Unsupported location input.");
|
|
4297
|
+
}
|
|
4298
|
+
async resolveStrongRefFromUri(uri) {
|
|
4299
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4300
|
+
if (!uriMatch) {
|
|
4301
|
+
throw new ValidationError(`resolveLocation: Invalid location AT-URI: "${uri}"`);
|
|
4302
|
+
}
|
|
4303
|
+
const [, repo, collection, rkey] = uriMatch;
|
|
4304
|
+
const record = await this.agent.com.atproto.repo.getRecord({ repo, collection, rkey });
|
|
4305
|
+
if (!record.success) {
|
|
4306
|
+
throw new NetworkError(`resolveLocation: getRecord failed for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4307
|
+
}
|
|
4308
|
+
if (!record.data.cid) {
|
|
4309
|
+
throw new NetworkError(`resolveLocation: getRecord returned no CID for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4310
|
+
}
|
|
4311
|
+
return { $type: "com.atproto.repo.strongRef", uri, cid: record.data.cid };
|
|
4312
|
+
}
|
|
4313
|
+
/**
|
|
4314
|
+
* Adds evidence to any subject via the subject ref.
|
|
3707
4315
|
*
|
|
3708
|
-
* @param
|
|
3709
|
-
* @param evidence - Array of evidence items to add
|
|
4316
|
+
* @param evidence - HypercertEvidenceInput
|
|
3710
4317
|
* @returns Promise resolving to update result
|
|
3711
4318
|
* @throws {@link ValidationError} if validation fails
|
|
3712
4319
|
* @throws {@link NetworkError} if the operation fails
|
|
3713
4320
|
*
|
|
3714
|
-
* @remarks
|
|
3715
|
-
* Evidence is appended to existing evidence, not replaced.
|
|
3716
|
-
*
|
|
3717
4321
|
* @example
|
|
3718
4322
|
* ```typescript
|
|
3719
|
-
* await repo.hypercerts.addEvidence(
|
|
3720
|
-
*
|
|
3721
|
-
*
|
|
3722
|
-
*
|
|
4323
|
+
* await repo.hypercerts.addEvidence({
|
|
4324
|
+
* subjectUri: "at://did:plc:u7h3dstby64di67bxaotzxcz/org.hypercerts.claim.activity/3mbvv5d7ixh2g"
|
|
4325
|
+
* content: Blob,
|
|
4326
|
+
* title: "Meeting Notes",
|
|
4327
|
+
* shortDescription: "Meetings notes from the 3rd of December 2025",
|
|
4328
|
+
* description: "The meeting with the board of directors and audience on 2025 in regards to the ecological landscape",
|
|
4329
|
+
* relationType: "supports",
|
|
4330
|
+
* })
|
|
3723
4331
|
* ```
|
|
3724
4332
|
*/
|
|
3725
|
-
async addEvidence(
|
|
4333
|
+
async addEvidence(evidence) {
|
|
3726
4334
|
try {
|
|
3727
|
-
const
|
|
3728
|
-
const
|
|
3729
|
-
const
|
|
3730
|
-
const
|
|
3731
|
-
|
|
3732
|
-
|
|
4335
|
+
const { subjectUri, content, ...rest } = evidence;
|
|
4336
|
+
const subject = await this.get(subjectUri);
|
|
4337
|
+
const createdAt = new Date().toISOString();
|
|
4338
|
+
const evidenceContent = await this.resolveUriOrBlob(content, "application/octet-stream");
|
|
4339
|
+
const evidenceRecord = {
|
|
4340
|
+
...rest,
|
|
4341
|
+
$type: HYPERCERT_COLLECTIONS.EVIDENCE,
|
|
4342
|
+
createdAt,
|
|
4343
|
+
content: evidenceContent,
|
|
4344
|
+
subject: { uri: subject.uri, cid: subject.cid },
|
|
4345
|
+
};
|
|
4346
|
+
const validation = lexicon.validate(evidenceRecord, HYPERCERT_COLLECTIONS.EVIDENCE, "main", false);
|
|
4347
|
+
if (!validation.success) {
|
|
4348
|
+
throw new ValidationError(`Invalid evidence record: ${validation.error?.message}`);
|
|
4349
|
+
}
|
|
4350
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4351
|
+
repo: this.repoDid,
|
|
4352
|
+
collection: HYPERCERT_COLLECTIONS.EVIDENCE,
|
|
4353
|
+
record: evidenceRecord,
|
|
3733
4354
|
});
|
|
3734
|
-
|
|
3735
|
-
|
|
4355
|
+
if (!result.success) {
|
|
4356
|
+
throw new NetworkError(`Failed to add evidence`);
|
|
4357
|
+
}
|
|
4358
|
+
this.emit("evidenceAdded", { uri: result.data.uri, cid: result.data.cid });
|
|
4359
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
3736
4360
|
}
|
|
3737
4361
|
catch (error) {
|
|
3738
4362
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -3741,22 +4365,29 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3741
4365
|
}
|
|
3742
4366
|
}
|
|
3743
4367
|
/**
|
|
3744
|
-
* Creates a contribution record.
|
|
4368
|
+
* Creates a contribution details record.
|
|
4369
|
+
*
|
|
4370
|
+
* This creates a standalone contribution details record that can be referenced
|
|
4371
|
+
* from an activity's `contributors` array via a strong reference.
|
|
3745
4372
|
*
|
|
3746
4373
|
* @param params - Contribution parameters
|
|
3747
|
-
* @param params.hypercertUri - Optional hypercert
|
|
3748
|
-
* @param params.contributors - Array of contributor DIDs
|
|
3749
|
-
* @param params.role - Role of the
|
|
4374
|
+
* @param params.hypercertUri - Optional hypercert (unused, kept for backward compatibility)
|
|
4375
|
+
* @param params.contributors - Array of contributor DIDs (unused, kept for backward compatibility)
|
|
4376
|
+
* @param params.role - Role of the contributor (e.g., "coordinator", "implementer")
|
|
3750
4377
|
* @param params.description - Optional description of the contribution
|
|
3751
|
-
* @returns Promise resolving to contribution record URI and CID
|
|
4378
|
+
* @returns Promise resolving to contribution details record URI and CID
|
|
3752
4379
|
* @throws {@link ValidationError} if validation fails
|
|
3753
4380
|
* @throws {@link NetworkError} if the operation fails
|
|
3754
4381
|
*
|
|
4382
|
+
* @remarks
|
|
4383
|
+
* In the new lexicon structure, contributions are stored differently:
|
|
4384
|
+
* - Use `contributionDetails` for detailed contribution records (role, description, timeframe)
|
|
4385
|
+
* - Use `contributorInformation` for contributor profiles (identifier, displayName, image)
|
|
4386
|
+
* - Reference these from the activity's `contributors` array using strong refs
|
|
4387
|
+
*
|
|
3755
4388
|
* @example
|
|
3756
4389
|
* ```typescript
|
|
3757
4390
|
* await repo.hypercerts.addContribution({
|
|
3758
|
-
* hypercertUri: hypercertUri,
|
|
3759
|
-
* contributors: ["did:plc:alice", "did:plc:bob"],
|
|
3760
4391
|
* role: "implementer",
|
|
3761
4392
|
* description: "On-ground implementation team",
|
|
3762
4393
|
* });
|
|
@@ -3766,28 +4397,22 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3766
4397
|
try {
|
|
3767
4398
|
const createdAt = new Date().toISOString();
|
|
3768
4399
|
const contributionRecord = {
|
|
3769
|
-
$type: HYPERCERT_COLLECTIONS.
|
|
3770
|
-
contributors: params.contributors,
|
|
4400
|
+
$type: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
|
|
3771
4401
|
role: params.role,
|
|
3772
4402
|
createdAt,
|
|
3773
|
-
|
|
3774
|
-
hypercert: { uri: "", cid: "" }, // Will be set below if hypercertUri provided
|
|
4403
|
+
contributionDescription: params.description,
|
|
3775
4404
|
};
|
|
3776
|
-
|
|
3777
|
-
const hypercert = await this.get(params.hypercertUri);
|
|
3778
|
-
contributionRecord.hypercert = { uri: hypercert.uri, cid: hypercert.cid };
|
|
3779
|
-
}
|
|
3780
|
-
const validation = lexicon.validate(contributionRecord, HYPERCERT_COLLECTIONS.CONTRIBUTION, "main", false);
|
|
4405
|
+
const validation = lexicon.validate(contributionRecord, HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS, "main", false);
|
|
3781
4406
|
if (!validation.success) {
|
|
3782
|
-
throw new ValidationError(`Invalid contribution record: ${validation.error?.message}`);
|
|
4407
|
+
throw new ValidationError(`Invalid contribution details record: ${validation.error?.message}`);
|
|
3783
4408
|
}
|
|
3784
4409
|
const result = await this.agent.com.atproto.repo.createRecord({
|
|
3785
4410
|
repo: this.repoDid,
|
|
3786
|
-
collection: HYPERCERT_COLLECTIONS.
|
|
4411
|
+
collection: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
|
|
3787
4412
|
record: contributionRecord,
|
|
3788
4413
|
});
|
|
3789
4414
|
if (!result.success) {
|
|
3790
|
-
throw new NetworkError("Failed to create contribution");
|
|
4415
|
+
throw new NetworkError("Failed to create contribution details");
|
|
3791
4416
|
}
|
|
3792
4417
|
this.emit("contributionCreated", { uri: result.data.uri, cid: result.data.cid });
|
|
3793
4418
|
return { uri: result.data.uri, cid: result.data.cid };
|
|
@@ -3922,9 +4547,9 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3922
4547
|
*
|
|
3923
4548
|
* @param params - Collection parameters
|
|
3924
4549
|
* @param params.title - Collection title
|
|
3925
|
-
* @param params.
|
|
4550
|
+
* @param params.items - Array of hypercert references with weights
|
|
3926
4551
|
* @param params.shortDescription - Optional short description
|
|
3927
|
-
* @param params.
|
|
4552
|
+
* @param params.banner - Optional cover image blob
|
|
3928
4553
|
* @returns Promise resolving to collection record URI and CID
|
|
3929
4554
|
* @throws {@link ValidationError} if validation fails
|
|
3930
4555
|
* @throws {@link NetworkError} if the operation fails
|
|
@@ -3934,66 +4559,64 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3934
4559
|
* const collection = await repo.hypercerts.createCollection({
|
|
3935
4560
|
* title: "Climate Projects 2024",
|
|
3936
4561
|
* shortDescription: "Our climate impact portfolio",
|
|
3937
|
-
*
|
|
3938
|
-
* { uri: hypercert1Uri, cid: hypercert1Cid,
|
|
3939
|
-
* { uri: hypercert2Uri, cid: hypercert2Cid,
|
|
3940
|
-
* { uri: hypercert3Uri, cid: hypercert3Cid,
|
|
4562
|
+
* items: [
|
|
4563
|
+
* { itemIdentifier: { uri: hypercert1Uri, cid: hypercert1Cid }, itemWeight: "0.5" },
|
|
4564
|
+
* { itemIdentifier: { uri: hypercert2Uri, cid: hypercert2Cid }, itemWeight: "0.3" },
|
|
4565
|
+
* { itemIdentifier: { uri: hypercert3Uri, cid: hypercert3Cid }, itemWeight: "0.2" },
|
|
3941
4566
|
* ],
|
|
3942
|
-
*
|
|
4567
|
+
* banner: coverImageBlob,
|
|
3943
4568
|
* });
|
|
3944
4569
|
* ```
|
|
3945
4570
|
*/
|
|
3946
4571
|
async createCollection(params) {
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
if (uploadResult.success) {
|
|
3957
|
-
coverPhotoRef = {
|
|
3958
|
-
$type: "blob",
|
|
3959
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
3960
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
3961
|
-
size: uploadResult.data.blob.size,
|
|
3962
|
-
};
|
|
3963
|
-
}
|
|
3964
|
-
}
|
|
3965
|
-
const collectionRecord = {
|
|
3966
|
-
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
3967
|
-
title: params.title,
|
|
3968
|
-
claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
|
|
3969
|
-
createdAt,
|
|
3970
|
-
};
|
|
3971
|
-
if (params.shortDescription) {
|
|
3972
|
-
collectionRecord.shortDescription = params.shortDescription;
|
|
3973
|
-
}
|
|
3974
|
-
if (coverPhotoRef) {
|
|
3975
|
-
collectionRecord.coverPhoto = coverPhotoRef;
|
|
3976
|
-
}
|
|
3977
|
-
const validation = lexicon.validate(collectionRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
3978
|
-
if (!validation.success) {
|
|
3979
|
-
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
3980
|
-
}
|
|
3981
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
3982
|
-
repo: this.repoDid,
|
|
3983
|
-
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
3984
|
-
record: collectionRecord,
|
|
3985
|
-
});
|
|
3986
|
-
if (!result.success) {
|
|
3987
|
-
throw new NetworkError("Failed to create collection");
|
|
3988
|
-
}
|
|
3989
|
-
this.emit("collectionCreated", { uri: result.data.uri, cid: result.data.cid });
|
|
3990
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
4572
|
+
const createdAt = new Date().toISOString();
|
|
4573
|
+
const collectionRecord = {
|
|
4574
|
+
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4575
|
+
title: params.title,
|
|
4576
|
+
items: [],
|
|
4577
|
+
createdAt,
|
|
4578
|
+
};
|
|
4579
|
+
if (params.type) {
|
|
4580
|
+
collectionRecord.type = params.type;
|
|
3991
4581
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
4582
|
+
if (params.shortDescription) {
|
|
4583
|
+
collectionRecord.shortDescription = params.shortDescription;
|
|
4584
|
+
}
|
|
4585
|
+
if (params.description) {
|
|
4586
|
+
collectionRecord.description = params.description;
|
|
4587
|
+
}
|
|
4588
|
+
if (params.avatar) {
|
|
4589
|
+
collectionRecord.avatar = await this.resolveCollectionImageInput(params.avatar);
|
|
4590
|
+
}
|
|
4591
|
+
if (params.banner) {
|
|
4592
|
+
collectionRecord.banner = await this.resolveCollectionImageInput(params.banner, true);
|
|
4593
|
+
}
|
|
4594
|
+
if (params.items !== undefined) {
|
|
4595
|
+
collectionRecord.items = params.items;
|
|
4596
|
+
}
|
|
4597
|
+
if (params.location) {
|
|
4598
|
+
const locationResult = await this.resolveLocation(params.location);
|
|
4599
|
+
collectionRecord.location = locationResult;
|
|
4600
|
+
}
|
|
4601
|
+
const validation = lexicon.validate(collectionRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4602
|
+
if (!validation.success) {
|
|
4603
|
+
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
4604
|
+
}
|
|
4605
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4606
|
+
repo: this.repoDid,
|
|
4607
|
+
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4608
|
+
record: collectionRecord,
|
|
4609
|
+
});
|
|
4610
|
+
if (!result.success) {
|
|
4611
|
+
throw new NetworkError("Failed to create collection");
|
|
3996
4612
|
}
|
|
4613
|
+
const createCollectionResult = {
|
|
4614
|
+
uri: result.data.uri,
|
|
4615
|
+
cid: result.data.cid,
|
|
4616
|
+
record: collectionRecord,
|
|
4617
|
+
};
|
|
4618
|
+
this.emit("collectionCreated", createCollectionResult);
|
|
4619
|
+
return createCollectionResult;
|
|
3997
4620
|
}
|
|
3998
4621
|
/**
|
|
3999
4622
|
* Gets a collection by its AT-URI.
|
|
@@ -4083,143 +4706,645 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4083
4706
|
throw new NetworkError(`Failed to list collections: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
4084
4707
|
}
|
|
4085
4708
|
}
|
|
4086
|
-
}
|
|
4087
|
-
|
|
4088
|
-
/**
|
|
4089
|
-
* CollaboratorOperationsImpl - SDS collaborator management operations.
|
|
4090
|
-
*
|
|
4091
|
-
* This module provides the implementation for managing collaborator
|
|
4092
|
-
* access on Shared Data Server (SDS) repositories.
|
|
4093
|
-
*
|
|
4094
|
-
* @packageDocumentation
|
|
4095
|
-
*/
|
|
4096
|
-
/**
|
|
4097
|
-
* Implementation of collaborator operations for SDS access control.
|
|
4098
|
-
*
|
|
4099
|
-
* This class manages access permissions for shared repositories on
|
|
4100
|
-
* Shared Data Servers (SDS). It provides role-based access control
|
|
4101
|
-
* with predefined permission sets.
|
|
4102
|
-
*
|
|
4103
|
-
* @remarks
|
|
4104
|
-
* This class is typically not instantiated directly. Access it through
|
|
4105
|
-
* {@link Repository.collaborators} on an SDS-connected repository.
|
|
4106
|
-
*
|
|
4107
|
-
* **Role Hierarchy**:
|
|
4108
|
-
* - `viewer`: Read-only access
|
|
4109
|
-
* - `editor`: Read + Create + Update
|
|
4110
|
-
* - `admin`: All permissions except ownership transfer
|
|
4111
|
-
* - `owner`: Full control including ownership management
|
|
4112
|
-
*
|
|
4113
|
-
* **SDS API Endpoints Used**:
|
|
4114
|
-
* - `com.sds.repo.grantAccess`: Grant access to a user
|
|
4115
|
-
* - `com.sds.repo.revokeAccess`: Revoke access from a user
|
|
4116
|
-
* - `com.sds.repo.listCollaborators`: List all collaborators
|
|
4117
|
-
* - `com.sds.repo.getPermissions`: Get current user's permissions
|
|
4118
|
-
* - `com.sds.repo.transferOwnership`: Transfer repository ownership
|
|
4119
|
-
*
|
|
4120
|
-
* @example
|
|
4121
|
-
* ```typescript
|
|
4122
|
-
* // Get SDS repository
|
|
4123
|
-
* const sdsRepo = sdk.repository(session, { server: "sds" });
|
|
4124
|
-
*
|
|
4125
|
-
* // Grant editor access
|
|
4126
|
-
* await sdsRepo.collaborators.grant({
|
|
4127
|
-
* userDid: "did:plc:new-user",
|
|
4128
|
-
* role: "editor",
|
|
4129
|
-
* });
|
|
4130
|
-
*
|
|
4131
|
-
* // List all collaborators
|
|
4132
|
-
* const collaborators = await sdsRepo.collaborators.list();
|
|
4133
|
-
*
|
|
4134
|
-
* // Check specific user
|
|
4135
|
-
* const hasAccess = await sdsRepo.collaborators.hasAccess("did:plc:someone");
|
|
4136
|
-
* const role = await sdsRepo.collaborators.getRole("did:plc:someone");
|
|
4137
|
-
* ```
|
|
4138
|
-
*
|
|
4139
|
-
* @internal
|
|
4140
|
-
*/
|
|
4141
|
-
class CollaboratorOperationsImpl {
|
|
4142
4709
|
/**
|
|
4143
|
-
* Creates a new
|
|
4710
|
+
* Creates a new project that organizes multiple hypercert activities.
|
|
4144
4711
|
*
|
|
4145
|
-
*
|
|
4146
|
-
*
|
|
4147
|
-
* @param serverUrl - SDS server URL
|
|
4712
|
+
* A project is a collection with type='project' and optional location sidecar.
|
|
4713
|
+
* This method delegates to createCollection and adds the type field.
|
|
4148
4714
|
*
|
|
4149
|
-
* @
|
|
4150
|
-
|
|
4151
|
-
constructor(session, repoDid, serverUrl) {
|
|
4152
|
-
this.session = session;
|
|
4153
|
-
this.repoDid = repoDid;
|
|
4154
|
-
this.serverUrl = serverUrl;
|
|
4155
|
-
}
|
|
4156
|
-
/**
|
|
4157
|
-
* Converts a role to its corresponding permissions object.
|
|
4715
|
+
* @param params - Project creation parameters
|
|
4716
|
+
* @returns Promise resolving to created project URI and CID with optional location URI
|
|
4158
4717
|
*
|
|
4159
|
-
* @
|
|
4160
|
-
*
|
|
4161
|
-
*
|
|
4718
|
+
* @example
|
|
4719
|
+
* ```typescript
|
|
4720
|
+
* const result = await repo.hypercerts.createProject({
|
|
4721
|
+
* title: "Climate Impact 2024",
|
|
4722
|
+
* shortDescription: "Year-long climate initiative",
|
|
4723
|
+
* items: [
|
|
4724
|
+
* { itemIdentifier: { uri: activity1Uri, cid: activity1Cid }, itemWeight: "0.6" },
|
|
4725
|
+
* { itemIdentifier: { uri: activity2Uri, cid: activity2Cid }, itemWeight: "0.4" }
|
|
4726
|
+
* ]
|
|
4727
|
+
* });
|
|
4728
|
+
* console.log(`Created project: ${result.uri}`);
|
|
4729
|
+
* ```
|
|
4162
4730
|
*/
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
return { read: true, create: true, update: true, delete: true, admin: true, owner: false };
|
|
4171
|
-
case "owner":
|
|
4172
|
-
return { read: true, create: true, update: true, delete: true, admin: true, owner: true };
|
|
4173
|
-
}
|
|
4731
|
+
async createProject(params) {
|
|
4732
|
+
const result = await this.createCollection({
|
|
4733
|
+
...params,
|
|
4734
|
+
type: "project",
|
|
4735
|
+
});
|
|
4736
|
+
this.emit("projectCreated", { uri: result.uri, cid: result.cid });
|
|
4737
|
+
return result;
|
|
4174
4738
|
}
|
|
4175
4739
|
/**
|
|
4176
|
-
*
|
|
4740
|
+
* Gets a project by its AT-URI.
|
|
4177
4741
|
*
|
|
4178
|
-
*
|
|
4179
|
-
* @returns The highest role matching the permissions
|
|
4180
|
-
* @internal
|
|
4181
|
-
*/
|
|
4182
|
-
permissionsToRole(permissions) {
|
|
4183
|
-
if (permissions.owner)
|
|
4184
|
-
return "owner";
|
|
4185
|
-
if (permissions.admin)
|
|
4186
|
-
return "admin";
|
|
4187
|
-
if (permissions.create || permissions.update)
|
|
4188
|
-
return "editor";
|
|
4189
|
-
return "viewer";
|
|
4190
|
-
}
|
|
4191
|
-
/**
|
|
4192
|
-
* Normalizes permissions from SDS API format to SDK format.
|
|
4742
|
+
* Projects are collections with `type='project'`.
|
|
4193
4743
|
*
|
|
4194
|
-
*
|
|
4195
|
-
*
|
|
4196
|
-
* This method ensures all expected fields are present with default values.
|
|
4744
|
+
* @param uri - AT-URI of the project
|
|
4745
|
+
* @returns Promise resolving to project data (as collection)
|
|
4197
4746
|
*
|
|
4198
|
-
* @
|
|
4199
|
-
*
|
|
4200
|
-
*
|
|
4747
|
+
* @example
|
|
4748
|
+
* ```typescript
|
|
4749
|
+
* const { record } = await repo.hypercerts.getProject(projectUri);
|
|
4750
|
+
* console.log(`${record.title}: ${record.items?.length || 0} activities`);
|
|
4751
|
+
* ```
|
|
4201
4752
|
*/
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4753
|
+
async getProject(uri) {
|
|
4754
|
+
try {
|
|
4755
|
+
// Parse URI
|
|
4756
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4757
|
+
if (!uriMatch) {
|
|
4758
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4759
|
+
}
|
|
4760
|
+
const [, , collection, rkey] = uriMatch;
|
|
4761
|
+
// Fetch record
|
|
4762
|
+
const result = await this.agent.com.atproto.repo.getRecord({
|
|
4763
|
+
repo: this.repoDid,
|
|
4764
|
+
collection,
|
|
4765
|
+
rkey,
|
|
4766
|
+
});
|
|
4767
|
+
if (!result.success) {
|
|
4768
|
+
throw new NetworkError("Failed to get project");
|
|
4769
|
+
}
|
|
4770
|
+
// Validate as collection
|
|
4771
|
+
const validation = lexicon.validate(result.data.value, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4772
|
+
if (!validation.success) {
|
|
4773
|
+
throw new ValidationError(`Invalid project record format: ${validation.error?.message}`);
|
|
4774
|
+
}
|
|
4775
|
+
// Verify it's actually a project (collection with type='project')
|
|
4776
|
+
const record = result.data.value;
|
|
4777
|
+
if (record.type !== "project") {
|
|
4778
|
+
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
4779
|
+
}
|
|
4780
|
+
return {
|
|
4781
|
+
uri: result.data.uri,
|
|
4782
|
+
cid: result.data.cid ?? "",
|
|
4783
|
+
record,
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4786
|
+
catch (error) {
|
|
4787
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
4788
|
+
throw error;
|
|
4789
|
+
throw new NetworkError(`Failed to get project: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
4790
|
+
}
|
|
4211
4791
|
}
|
|
4212
4792
|
/**
|
|
4213
|
-
*
|
|
4793
|
+
* Lists all projects with optional pagination.
|
|
4214
4794
|
*
|
|
4215
|
-
*
|
|
4216
|
-
*
|
|
4217
|
-
* @param params.role - Role to assign (determines permissions)
|
|
4218
|
-
* @throws {@link NetworkError} if the grant operation fails
|
|
4795
|
+
* Projects are collections with `type='project'`. This method filters
|
|
4796
|
+
* collections to only return those with type='project'.
|
|
4219
4797
|
*
|
|
4220
|
-
* @
|
|
4221
|
-
*
|
|
4222
|
-
*
|
|
4798
|
+
* @param params - Optional pagination parameters
|
|
4799
|
+
* @returns Promise resolving to paginated list of projects
|
|
4800
|
+
*
|
|
4801
|
+
* @example
|
|
4802
|
+
* ```typescript
|
|
4803
|
+
* const { records } = await repo.hypercerts.listProjects();
|
|
4804
|
+
* for (const { record } of records) {
|
|
4805
|
+
* console.log(`${record.title}: ${record.shortDescription}`);
|
|
4806
|
+
* }
|
|
4807
|
+
* ```
|
|
4808
|
+
*/
|
|
4809
|
+
async listProjects(params) {
|
|
4810
|
+
try {
|
|
4811
|
+
const limit = params?.limit;
|
|
4812
|
+
let cursor = params?.cursor;
|
|
4813
|
+
const allRecords = [];
|
|
4814
|
+
// Loop-fetch until we have enough projects or no more cursor
|
|
4815
|
+
while (!cursor || allRecords.length < (limit ?? Infinity)) {
|
|
4816
|
+
const result = await this.agent.com.atproto.repo.listRecords({
|
|
4817
|
+
repo: this.repoDid,
|
|
4818
|
+
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4819
|
+
limit: limit ?? 50,
|
|
4820
|
+
cursor,
|
|
4821
|
+
});
|
|
4822
|
+
if (!result.success) {
|
|
4823
|
+
throw new NetworkError("Failed to list projects");
|
|
4824
|
+
}
|
|
4825
|
+
// Filter and collect project records
|
|
4826
|
+
for (const r of result.data.records ?? []) {
|
|
4827
|
+
const record = r.value;
|
|
4828
|
+
if (record.type === "project") {
|
|
4829
|
+
allRecords.push({ uri: r.uri, cid: r.cid, record });
|
|
4830
|
+
}
|
|
4831
|
+
// Stop if we've collected enough
|
|
4832
|
+
if (limit && allRecords.length >= limit)
|
|
4833
|
+
break;
|
|
4834
|
+
}
|
|
4835
|
+
// Update cursor; break if no more pages
|
|
4836
|
+
cursor = result.data.cursor;
|
|
4837
|
+
if (!cursor)
|
|
4838
|
+
break;
|
|
4839
|
+
}
|
|
4840
|
+
return { records: allRecords, cursor };
|
|
4841
|
+
}
|
|
4842
|
+
catch (error) {
|
|
4843
|
+
if (error instanceof NetworkError)
|
|
4844
|
+
throw error;
|
|
4845
|
+
throw new NetworkError(`Failed to list projects: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
/**
|
|
4849
|
+
* Updates an existing project.
|
|
4850
|
+
*
|
|
4851
|
+
* A project is a collection with type='project'. This method delegates to
|
|
4852
|
+
* updateCollection and handles the avatar field.
|
|
4853
|
+
*
|
|
4854
|
+
* @param uri - AT-URI of the project to update
|
|
4855
|
+
* @param updates - Fields to update
|
|
4856
|
+
* @returns Promise resolving to updated project URI and CID
|
|
4857
|
+
*
|
|
4858
|
+
* @example
|
|
4859
|
+
* ```typescript
|
|
4860
|
+
* const result = await repo.hypercerts.updateProject(projectUri, {
|
|
4861
|
+
* title: "Updated Project Title",
|
|
4862
|
+
* shortDescription: "New description"
|
|
4863
|
+
* });
|
|
4864
|
+
* ```
|
|
4865
|
+
*/
|
|
4866
|
+
async updateProject(uri, updates) {
|
|
4867
|
+
// Verify it's a project before updating
|
|
4868
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4869
|
+
if (!uriMatch) {
|
|
4870
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4871
|
+
}
|
|
4872
|
+
const [, , collection, rkey] = uriMatch;
|
|
4873
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4874
|
+
repo: this.repoDid,
|
|
4875
|
+
collection,
|
|
4876
|
+
rkey,
|
|
4877
|
+
});
|
|
4878
|
+
if (!existing.success) {
|
|
4879
|
+
throw new NetworkError(`Project not found: ${uri}`);
|
|
4880
|
+
}
|
|
4881
|
+
const record = existing.data.value;
|
|
4882
|
+
if (record.type !== "project") {
|
|
4883
|
+
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
4884
|
+
}
|
|
4885
|
+
// Delegate to updateCollection
|
|
4886
|
+
const result = await this.updateCollection(uri, updates);
|
|
4887
|
+
this.emit("projectUpdated", { uri: result.uri, cid: result.cid });
|
|
4888
|
+
return result;
|
|
4889
|
+
}
|
|
4890
|
+
/**
|
|
4891
|
+
* Deletes a project.
|
|
4892
|
+
*
|
|
4893
|
+
* A project is a collection with type='project'. This method delegates to
|
|
4894
|
+
* deleteCollection after verifying the record is a project.
|
|
4895
|
+
*
|
|
4896
|
+
* @param uri - AT-URI of the project to delete
|
|
4897
|
+
*
|
|
4898
|
+
* @example
|
|
4899
|
+
* ```typescript
|
|
4900
|
+
* await repo.hypercerts.deleteProject(projectUri);
|
|
4901
|
+
* console.log("Project deleted");
|
|
4902
|
+
* ```
|
|
4903
|
+
*/
|
|
4904
|
+
async deleteProject(uri) {
|
|
4905
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4906
|
+
if (!uriMatch) {
|
|
4907
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4908
|
+
}
|
|
4909
|
+
const [, , collection, rkey] = uriMatch;
|
|
4910
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4911
|
+
repo: this.repoDid,
|
|
4912
|
+
collection,
|
|
4913
|
+
rkey,
|
|
4914
|
+
});
|
|
4915
|
+
if (!existing.success) {
|
|
4916
|
+
throw new NetworkError(`Project not found: ${uri}`);
|
|
4917
|
+
}
|
|
4918
|
+
const record = existing.data.value;
|
|
4919
|
+
if (record.type !== "project") {
|
|
4920
|
+
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
4921
|
+
}
|
|
4922
|
+
await this.deleteCollection(uri);
|
|
4923
|
+
this.emit("projectDeleted", { uri });
|
|
4924
|
+
}
|
|
4925
|
+
/**
|
|
4926
|
+
* Updates a collection.
|
|
4927
|
+
*
|
|
4928
|
+
* @param uri - AT-URI of the collection to update
|
|
4929
|
+
* @param updates - Fields to update
|
|
4930
|
+
* @returns Promise resolving to updated collection URI and CID
|
|
4931
|
+
*/
|
|
4932
|
+
async updateCollection(uri, updates) {
|
|
4933
|
+
try {
|
|
4934
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4935
|
+
if (!uriMatch) {
|
|
4936
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4937
|
+
}
|
|
4938
|
+
const [, , collection, rkey] = uriMatch;
|
|
4939
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4940
|
+
repo: this.repoDid,
|
|
4941
|
+
collection,
|
|
4942
|
+
rkey,
|
|
4943
|
+
});
|
|
4944
|
+
if (!existing.success) {
|
|
4945
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
4946
|
+
}
|
|
4947
|
+
const existingRecord = existing.data.value;
|
|
4948
|
+
const recordForUpdate = {
|
|
4949
|
+
...existingRecord,
|
|
4950
|
+
createdAt: existingRecord.createdAt,
|
|
4951
|
+
type: existingRecord.type,
|
|
4952
|
+
};
|
|
4953
|
+
if (updates.title !== undefined)
|
|
4954
|
+
recordForUpdate.title = updates.title;
|
|
4955
|
+
if (updates.shortDescription !== undefined)
|
|
4956
|
+
recordForUpdate.shortDescription = updates.shortDescription;
|
|
4957
|
+
if (updates.description !== undefined)
|
|
4958
|
+
recordForUpdate.description = updates.description;
|
|
4959
|
+
// Explicitly reject type changes
|
|
4960
|
+
if (updates.type !== undefined && updates.type !== existingRecord.type) {
|
|
4961
|
+
throw new ValidationError(`Cannot change collection type from '${existingRecord.type}' to '${updates.type}'`);
|
|
4962
|
+
}
|
|
4963
|
+
delete recordForUpdate.avatar;
|
|
4964
|
+
if (updates.avatar !== undefined) {
|
|
4965
|
+
if (updates.avatar === null) {
|
|
4966
|
+
// Remove avatar
|
|
4967
|
+
}
|
|
4968
|
+
else {
|
|
4969
|
+
recordForUpdate.avatar = await this.resolveCollectionImageInput(updates.avatar);
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
else if (existingRecord.avatar) {
|
|
4973
|
+
recordForUpdate.avatar = existingRecord.avatar;
|
|
4974
|
+
}
|
|
4975
|
+
delete recordForUpdate.banner;
|
|
4976
|
+
if (updates.banner !== undefined) {
|
|
4977
|
+
if (updates.banner === null) {
|
|
4978
|
+
// Remove banner
|
|
4979
|
+
}
|
|
4980
|
+
else {
|
|
4981
|
+
recordForUpdate.banner = await this.resolveCollectionImageInput(updates.banner, true);
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
else if (existingRecord.banner) {
|
|
4985
|
+
recordForUpdate.banner = existingRecord.banner;
|
|
4986
|
+
}
|
|
4987
|
+
delete recordForUpdate.location;
|
|
4988
|
+
if (updates.location !== undefined) {
|
|
4989
|
+
if (updates.location === null) {
|
|
4990
|
+
// Remove location
|
|
4991
|
+
}
|
|
4992
|
+
else {
|
|
4993
|
+
const resolvedLocation = await this.resolveLocation(updates.location);
|
|
4994
|
+
if (!resolvedLocation) {
|
|
4995
|
+
throw new ValidationError("resolveLocation: failed to resolve location");
|
|
4996
|
+
}
|
|
4997
|
+
recordForUpdate.location = resolvedLocation;
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
else if (existingRecord.location) {
|
|
5001
|
+
recordForUpdate.location = existingRecord.location;
|
|
5002
|
+
}
|
|
5003
|
+
if (updates.items) {
|
|
5004
|
+
recordForUpdate.items = updates.items;
|
|
5005
|
+
}
|
|
5006
|
+
else if (existingRecord.items) {
|
|
5007
|
+
recordForUpdate.items = existingRecord.items;
|
|
5008
|
+
}
|
|
5009
|
+
const validation = lexicon.validate(recordForUpdate, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
5010
|
+
if (!validation.success) {
|
|
5011
|
+
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
5012
|
+
}
|
|
5013
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
5014
|
+
repo: this.repoDid,
|
|
5015
|
+
collection,
|
|
5016
|
+
rkey,
|
|
5017
|
+
record: recordForUpdate,
|
|
5018
|
+
});
|
|
5019
|
+
if (!result.success) {
|
|
5020
|
+
throw new NetworkError("Failed to update collection");
|
|
5021
|
+
}
|
|
5022
|
+
this.emit("collectionUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
5023
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
5024
|
+
}
|
|
5025
|
+
catch (error) {
|
|
5026
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5027
|
+
throw error;
|
|
5028
|
+
throw new NetworkError(`Failed to update collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
/**
|
|
5032
|
+
* Deletes a collection.
|
|
5033
|
+
*
|
|
5034
|
+
* @param uri - AT-URI of the collection to delete
|
|
5035
|
+
*/
|
|
5036
|
+
async deleteCollection(uri) {
|
|
5037
|
+
try {
|
|
5038
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5039
|
+
if (!uriMatch) {
|
|
5040
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5041
|
+
}
|
|
5042
|
+
const [, , collection, rkey] = uriMatch;
|
|
5043
|
+
const result = await this.agent.com.atproto.repo.deleteRecord({
|
|
5044
|
+
repo: this.repoDid,
|
|
5045
|
+
collection,
|
|
5046
|
+
rkey,
|
|
5047
|
+
});
|
|
5048
|
+
if (!result.success) {
|
|
5049
|
+
throw new NetworkError("Failed to delete collection");
|
|
5050
|
+
}
|
|
5051
|
+
this.emit("collectionDeleted", { uri });
|
|
5052
|
+
}
|
|
5053
|
+
catch (error) {
|
|
5054
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5055
|
+
throw error;
|
|
5056
|
+
throw new NetworkError(`Failed to delete collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
/**
|
|
5060
|
+
* Attaches a location to a collection.
|
|
5061
|
+
*
|
|
5062
|
+
* @param uri - AT-URI of the collection
|
|
5063
|
+
* @param location - Location data
|
|
5064
|
+
* @returns Promise resolving to location record result
|
|
5065
|
+
*/
|
|
5066
|
+
async attachLocationToCollection(uri, location) {
|
|
5067
|
+
try {
|
|
5068
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5069
|
+
if (!uriMatch) {
|
|
5070
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5071
|
+
}
|
|
5072
|
+
const [, , collection, rkey] = uriMatch;
|
|
5073
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5074
|
+
repo: this.repoDid,
|
|
5075
|
+
collection,
|
|
5076
|
+
rkey,
|
|
5077
|
+
});
|
|
5078
|
+
if (!existing.success) {
|
|
5079
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5080
|
+
}
|
|
5081
|
+
const resolvedLocation = await this.resolveLocation(location);
|
|
5082
|
+
if (!resolvedLocation) {
|
|
5083
|
+
throw new ValidationError("attachLocationToCollection: failed to resolve location");
|
|
5084
|
+
}
|
|
5085
|
+
const recordForUpdate = {
|
|
5086
|
+
...existing.data.value,
|
|
5087
|
+
location: resolvedLocation,
|
|
5088
|
+
};
|
|
5089
|
+
const updateResult = await this.agent.com.atproto.repo.putRecord({
|
|
5090
|
+
repo: this.repoDid,
|
|
5091
|
+
collection,
|
|
5092
|
+
rkey,
|
|
5093
|
+
record: recordForUpdate,
|
|
5094
|
+
});
|
|
5095
|
+
if (!updateResult.success) {
|
|
5096
|
+
throw new NetworkError("Failed to update collection with location");
|
|
5097
|
+
}
|
|
5098
|
+
this.emit("locationAttachedToCollection", {
|
|
5099
|
+
uri: resolvedLocation.uri,
|
|
5100
|
+
cid: resolvedLocation.cid,
|
|
5101
|
+
collectionUri: uri,
|
|
5102
|
+
});
|
|
5103
|
+
return { uri: resolvedLocation.uri, cid: resolvedLocation.cid };
|
|
5104
|
+
}
|
|
5105
|
+
catch (error) {
|
|
5106
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5107
|
+
throw error;
|
|
5108
|
+
throw new NetworkError(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* Removes a location from a collection.
|
|
5113
|
+
*
|
|
5114
|
+
* @param uri - AT-URI of the collection
|
|
5115
|
+
*/
|
|
5116
|
+
async removeLocationFromCollection(uri) {
|
|
5117
|
+
try {
|
|
5118
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5119
|
+
if (!uriMatch) {
|
|
5120
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5121
|
+
}
|
|
5122
|
+
const [, , collection, rkey] = uriMatch;
|
|
5123
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5124
|
+
repo: this.repoDid,
|
|
5125
|
+
collection,
|
|
5126
|
+
rkey,
|
|
5127
|
+
});
|
|
5128
|
+
if (!existing.success) {
|
|
5129
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5130
|
+
}
|
|
5131
|
+
const recordForUpdate = { ...existing.data.value };
|
|
5132
|
+
delete recordForUpdate.location;
|
|
5133
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
5134
|
+
repo: this.repoDid,
|
|
5135
|
+
collection,
|
|
5136
|
+
rkey,
|
|
5137
|
+
record: recordForUpdate,
|
|
5138
|
+
});
|
|
5139
|
+
if (!result.success) {
|
|
5140
|
+
throw new NetworkError("Failed to remove location from collection");
|
|
5141
|
+
}
|
|
5142
|
+
this.emit("locationRemovedFromCollection", { collectionUri: uri });
|
|
5143
|
+
}
|
|
5144
|
+
catch (error) {
|
|
5145
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5146
|
+
throw error;
|
|
5147
|
+
throw new NetworkError(`Failed to remove location: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
/**
|
|
5151
|
+
* Attaches a location to a project.
|
|
5152
|
+
*
|
|
5153
|
+
* @param uri - AT-URI of the project
|
|
5154
|
+
* @param location - Location data
|
|
5155
|
+
* @returns Promise resolving to location record result
|
|
5156
|
+
*/
|
|
5157
|
+
async attachLocationToProject(uri, location) {
|
|
5158
|
+
const result = await this.attachLocationToCollection(uri, location);
|
|
5159
|
+
this.emit("locationAttachedToProject", {
|
|
5160
|
+
uri: result.uri,
|
|
5161
|
+
cid: result.cid,
|
|
5162
|
+
projectUri: uri,
|
|
5163
|
+
});
|
|
5164
|
+
return result;
|
|
5165
|
+
}
|
|
5166
|
+
/**
|
|
5167
|
+
* Removes a location from a project.
|
|
5168
|
+
*
|
|
5169
|
+
* @param uri - AT-URI of the project
|
|
5170
|
+
*/
|
|
5171
|
+
async removeLocationFromProject(uri) {
|
|
5172
|
+
await this.removeLocationFromCollection(uri);
|
|
5173
|
+
this.emit("locationRemovedFromProject", { projectUri: uri });
|
|
5174
|
+
}
|
|
5175
|
+
/**
|
|
5176
|
+
* Creates an app.certified.location record.
|
|
5177
|
+
*
|
|
5178
|
+
* @param location - Location parameters
|
|
5179
|
+
* @returns Promise resolving to location record URI and CID
|
|
5180
|
+
*/
|
|
5181
|
+
async createLocationRecord(location) {
|
|
5182
|
+
if (!location.srs) {
|
|
5183
|
+
throw new ValidationError("srs (Spatial Reference System) is required. Example: 'EPSG:4326' for WGS84 coordinates, or 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' for CRS84.");
|
|
5184
|
+
}
|
|
5185
|
+
const { $type, createdAt, location: locationValue, lpVersion, ...rest } = location;
|
|
5186
|
+
if (locationValue === undefined) {
|
|
5187
|
+
throw new ValidationError("location is required to create a location record.");
|
|
5188
|
+
}
|
|
5189
|
+
const resolvedLocationValue = await this.resolveLocationValue(locationValue);
|
|
5190
|
+
const locationRecord = {
|
|
5191
|
+
...rest,
|
|
5192
|
+
lpVersion: lpVersion ?? "1.0",
|
|
5193
|
+
$type: $type ?? HYPERCERT_COLLECTIONS.LOCATION,
|
|
5194
|
+
createdAt: createdAt ?? new Date().toISOString(),
|
|
5195
|
+
location: resolvedLocationValue,
|
|
5196
|
+
};
|
|
5197
|
+
const validation = lexicon.validate(locationRecord, HYPERCERT_COLLECTIONS.LOCATION, "main", false);
|
|
5198
|
+
if (!validation.success) {
|
|
5199
|
+
throw new ValidationError(`Invalid location record: ${validation.error?.message}`);
|
|
5200
|
+
}
|
|
5201
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
5202
|
+
repo: this.repoDid,
|
|
5203
|
+
collection: HYPERCERT_COLLECTIONS.LOCATION,
|
|
5204
|
+
record: locationRecord,
|
|
5205
|
+
});
|
|
5206
|
+
if (!result.success) {
|
|
5207
|
+
throw new NetworkError("Failed to create location record");
|
|
5208
|
+
}
|
|
5209
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
/**
|
|
5214
|
+
* CollaboratorOperationsImpl - SDS collaborator management operations.
|
|
5215
|
+
*
|
|
5216
|
+
* This module provides the implementation for managing collaborator
|
|
5217
|
+
* access on Shared Data Server (SDS) repositories.
|
|
5218
|
+
*
|
|
5219
|
+
* @packageDocumentation
|
|
5220
|
+
*/
|
|
5221
|
+
/**
|
|
5222
|
+
* Implementation of collaborator operations for SDS access control.
|
|
5223
|
+
*
|
|
5224
|
+
* This class manages access permissions for shared repositories on
|
|
5225
|
+
* Shared Data Servers (SDS). It provides role-based access control
|
|
5226
|
+
* with predefined permission sets.
|
|
5227
|
+
*
|
|
5228
|
+
* @remarks
|
|
5229
|
+
* This class is typically not instantiated directly. Access it through
|
|
5230
|
+
* {@link Repository.collaborators} on an SDS-connected repository.
|
|
5231
|
+
*
|
|
5232
|
+
* **Role Hierarchy**:
|
|
5233
|
+
* - `viewer`: Read-only access
|
|
5234
|
+
* - `editor`: Read + Create + Update
|
|
5235
|
+
* - `admin`: All permissions except ownership transfer
|
|
5236
|
+
* - `owner`: Full control including ownership management
|
|
5237
|
+
*
|
|
5238
|
+
* **SDS API Endpoints Used**:
|
|
5239
|
+
* - `com.sds.repo.grantAccess`: Grant access to a user
|
|
5240
|
+
* - `com.sds.repo.revokeAccess`: Revoke access from a user
|
|
5241
|
+
* - `com.sds.repo.listCollaborators`: List all collaborators
|
|
5242
|
+
* - `com.sds.repo.getPermissions`: Get current user's permissions
|
|
5243
|
+
* - `com.sds.repo.transferOwnership`: Transfer repository ownership
|
|
5244
|
+
*
|
|
5245
|
+
* @example
|
|
5246
|
+
* ```typescript
|
|
5247
|
+
* // Get SDS repository
|
|
5248
|
+
* const sdsRepo = sdk.repository(session, { server: "sds" });
|
|
5249
|
+
*
|
|
5250
|
+
* // Grant editor access
|
|
5251
|
+
* await sdsRepo.collaborators.grant({
|
|
5252
|
+
* userDid: "did:plc:new-user",
|
|
5253
|
+
* role: "editor",
|
|
5254
|
+
* });
|
|
5255
|
+
*
|
|
5256
|
+
* // List all collaborators
|
|
5257
|
+
* const collaborators = await sdsRepo.collaborators.list();
|
|
5258
|
+
*
|
|
5259
|
+
* // Check specific user
|
|
5260
|
+
* const hasAccess = await sdsRepo.collaborators.hasAccess("did:plc:someone");
|
|
5261
|
+
* const role = await sdsRepo.collaborators.getRole("did:plc:someone");
|
|
5262
|
+
* ```
|
|
5263
|
+
*
|
|
5264
|
+
* @internal
|
|
5265
|
+
*/
|
|
5266
|
+
class CollaboratorOperationsImpl {
|
|
5267
|
+
/**
|
|
5268
|
+
* Creates a new CollaboratorOperationsImpl.
|
|
5269
|
+
*
|
|
5270
|
+
* @param session - Authenticated OAuth session with fetchHandler
|
|
5271
|
+
* @param repoDid - DID of the repository to manage
|
|
5272
|
+
* @param serverUrl - SDS server URL
|
|
5273
|
+
*
|
|
5274
|
+
* @internal
|
|
5275
|
+
*/
|
|
5276
|
+
constructor(session, repoDid, serverUrl) {
|
|
5277
|
+
this.session = session;
|
|
5278
|
+
this.repoDid = repoDid;
|
|
5279
|
+
this.serverUrl = serverUrl;
|
|
5280
|
+
}
|
|
5281
|
+
/**
|
|
5282
|
+
* Converts a role to its corresponding permissions object.
|
|
5283
|
+
*
|
|
5284
|
+
* @param role - The role to convert
|
|
5285
|
+
* @returns Permission flags for the role
|
|
5286
|
+
* @internal
|
|
5287
|
+
*/
|
|
5288
|
+
roleToPermissions(role) {
|
|
5289
|
+
switch (role) {
|
|
5290
|
+
case "viewer":
|
|
5291
|
+
return { read: true, create: false, update: false, delete: false, admin: false, owner: false };
|
|
5292
|
+
case "editor":
|
|
5293
|
+
return { read: true, create: true, update: true, delete: false, admin: false, owner: false };
|
|
5294
|
+
case "admin":
|
|
5295
|
+
return { read: true, create: true, update: true, delete: true, admin: true, owner: false };
|
|
5296
|
+
case "owner":
|
|
5297
|
+
return { read: true, create: true, update: true, delete: true, admin: true, owner: true };
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
/**
|
|
5301
|
+
* Determines the role from a permissions object.
|
|
5302
|
+
*
|
|
5303
|
+
* @param permissions - The permissions to analyze
|
|
5304
|
+
* @returns The highest role matching the permissions
|
|
5305
|
+
* @internal
|
|
5306
|
+
*/
|
|
5307
|
+
permissionsToRole(permissions) {
|
|
5308
|
+
if (permissions.owner)
|
|
5309
|
+
return "owner";
|
|
5310
|
+
if (permissions.admin)
|
|
5311
|
+
return "admin";
|
|
5312
|
+
if (permissions.create || permissions.update)
|
|
5313
|
+
return "editor";
|
|
5314
|
+
return "viewer";
|
|
5315
|
+
}
|
|
5316
|
+
/**
|
|
5317
|
+
* Normalizes permissions from SDS API format to SDK format.
|
|
5318
|
+
*
|
|
5319
|
+
* The SDS API returns permissions as an object with boolean flags
|
|
5320
|
+
* (e.g., `{ read: true, create: true, update: false, ... }`).
|
|
5321
|
+
* This method ensures all expected fields are present with default values.
|
|
5322
|
+
*
|
|
5323
|
+
* @param permissions - Permissions object from SDS API
|
|
5324
|
+
* @returns Normalized permission flags object
|
|
5325
|
+
* @internal
|
|
5326
|
+
*/
|
|
5327
|
+
parsePermissions(permissions) {
|
|
5328
|
+
return {
|
|
5329
|
+
read: permissions.read ?? false,
|
|
5330
|
+
create: permissions.create ?? false,
|
|
5331
|
+
update: permissions.update ?? false,
|
|
5332
|
+
delete: permissions.delete ?? false,
|
|
5333
|
+
admin: permissions.admin ?? false,
|
|
5334
|
+
owner: permissions.owner ?? false,
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
/**
|
|
5338
|
+
* Grants repository access to a user.
|
|
5339
|
+
*
|
|
5340
|
+
* @param params - Grant parameters
|
|
5341
|
+
* @param params.userDid - DID of the user to grant access to
|
|
5342
|
+
* @param params.role - Role to assign (determines permissions)
|
|
5343
|
+
* @throws {@link NetworkError} if the grant operation fails
|
|
5344
|
+
*
|
|
5345
|
+
* @remarks
|
|
5346
|
+
* If the user already has access, their permissions are updated
|
|
5347
|
+
* to the new role.
|
|
4223
5348
|
*
|
|
4224
5349
|
* @example
|
|
4225
5350
|
* ```typescript
|
|
@@ -4833,6 +5958,7 @@ class Repository {
|
|
|
4833
5958
|
* @param repoDid - DID of the repository to operate on
|
|
4834
5959
|
* @param isSDS - Whether this is a Shared Data Server
|
|
4835
5960
|
* @param logger - Optional logger for debugging
|
|
5961
|
+
* @param lexiconRegistry - Registry for custom lexicon management
|
|
4836
5962
|
*
|
|
4837
5963
|
* @remarks
|
|
4838
5964
|
* This constructor is typically not called directly. Use
|
|
@@ -4840,12 +5966,13 @@ class Repository {
|
|
|
4840
5966
|
*
|
|
4841
5967
|
* @internal
|
|
4842
5968
|
*/
|
|
4843
|
-
constructor(session, serverUrl, repoDid, isSDS, logger) {
|
|
5969
|
+
constructor(session, serverUrl, repoDid, isSDS, logger, lexiconRegistry) {
|
|
4844
5970
|
this.session = session;
|
|
4845
5971
|
this.serverUrl = serverUrl;
|
|
4846
5972
|
this.repoDid = repoDid;
|
|
4847
5973
|
this._isSDS = isSDS;
|
|
4848
5974
|
this.logger = logger;
|
|
5975
|
+
this.lexiconRegistry = lexiconRegistry || new LexiconRegistry();
|
|
4849
5976
|
// Create a ConfigurableAgent that routes requests to the specified server URL
|
|
4850
5977
|
// This allows routing to PDS, SDS, or any custom server while maintaining
|
|
4851
5978
|
// the OAuth session's authentication
|
|
@@ -4920,7 +6047,53 @@ class Repository {
|
|
|
4920
6047
|
* ```
|
|
4921
6048
|
*/
|
|
4922
6049
|
repo(did) {
|
|
4923
|
-
return new Repository(this.session, this.serverUrl, did, this._isSDS, this.logger);
|
|
6050
|
+
return new Repository(this.session, this.serverUrl, did, this._isSDS, this.logger, this.lexiconRegistry);
|
|
6051
|
+
}
|
|
6052
|
+
/**
|
|
6053
|
+
* Gets the LexiconRegistry instance for managing custom lexicons.
|
|
6054
|
+
*
|
|
6055
|
+
* The registry is shared across all operations in this repository and
|
|
6056
|
+
* enables validation of custom record types.
|
|
6057
|
+
*
|
|
6058
|
+
* @returns The {@link LexiconRegistry} instance
|
|
6059
|
+
*
|
|
6060
|
+
* @example
|
|
6061
|
+
* ```typescript
|
|
6062
|
+
* // Access the registry
|
|
6063
|
+
* const registry = repo.getLexiconRegistry();
|
|
6064
|
+
*
|
|
6065
|
+
* // Register a custom lexicon
|
|
6066
|
+
* registry.register({
|
|
6067
|
+
* lexicon: 1,
|
|
6068
|
+
* id: "org.myapp.evaluation",
|
|
6069
|
+
* defs: {
|
|
6070
|
+
* main: {
|
|
6071
|
+
* type: "record",
|
|
6072
|
+
* key: "tid",
|
|
6073
|
+
* record: {
|
|
6074
|
+
* type: "object",
|
|
6075
|
+
* required: ["$type", "score"],
|
|
6076
|
+
* properties: {
|
|
6077
|
+
* "$type": { type: "string", const: "org.myapp.evaluation" },
|
|
6078
|
+
* score: { type: "integer", minimum: 0, maximum: 100 }
|
|
6079
|
+
* }
|
|
6080
|
+
* }
|
|
6081
|
+
* }
|
|
6082
|
+
* }
|
|
6083
|
+
* });
|
|
6084
|
+
*
|
|
6085
|
+
* // Now create records using the custom lexicon
|
|
6086
|
+
* await repo.records.create({
|
|
6087
|
+
* collection: "org.myapp.evaluation",
|
|
6088
|
+
* record: {
|
|
6089
|
+
* $type: "org.myapp.evaluation",
|
|
6090
|
+
* score: 85
|
|
6091
|
+
* }
|
|
6092
|
+
* });
|
|
6093
|
+
* ```
|
|
6094
|
+
*/
|
|
6095
|
+
getLexiconRegistry() {
|
|
6096
|
+
return this.lexiconRegistry;
|
|
4924
6097
|
}
|
|
4925
6098
|
/**
|
|
4926
6099
|
* Low-level record operations for CRUD on any AT Protocol record type.
|
|
@@ -4953,7 +6126,7 @@ class Repository {
|
|
|
4953
6126
|
*/
|
|
4954
6127
|
get records() {
|
|
4955
6128
|
if (!this._records) {
|
|
4956
|
-
this._records = new RecordOperationsImpl(this.agent, this.repoDid);
|
|
6129
|
+
this._records = new RecordOperationsImpl(this.agent, this.repoDid, this.lexiconRegistry);
|
|
4957
6130
|
}
|
|
4958
6131
|
return this._records;
|
|
4959
6132
|
}
|
|
@@ -5130,26 +6303,67 @@ class Repository {
|
|
|
5130
6303
|
}
|
|
5131
6304
|
}
|
|
5132
6305
|
|
|
6306
|
+
/**
|
|
6307
|
+
* Custom URL validator that allows HTTP loopback addresses for development.
|
|
6308
|
+
*
|
|
6309
|
+
* Accepts:
|
|
6310
|
+
* - Any HTTPS URL (production)
|
|
6311
|
+
* - http://localhost (with optional port and path)
|
|
6312
|
+
* - http://127.0.0.1 (with optional port and path)
|
|
6313
|
+
* - http://[::1] (with optional port and path) - IPv6 loopback
|
|
6314
|
+
*
|
|
6315
|
+
* Rejects:
|
|
6316
|
+
* - Other HTTP URLs (e.g., http://example.com)
|
|
6317
|
+
* - Invalid URLs
|
|
6318
|
+
*
|
|
6319
|
+
* @internal
|
|
6320
|
+
*/
|
|
6321
|
+
const urlOrLoopback = zod.z.string().refine((value) => {
|
|
6322
|
+
try {
|
|
6323
|
+
const url = new URL(value);
|
|
6324
|
+
// Always allow HTTPS
|
|
6325
|
+
if (url.protocol === "https:") {
|
|
6326
|
+
return true;
|
|
6327
|
+
}
|
|
6328
|
+
// For HTTP, only allow loopback addresses
|
|
6329
|
+
if (url.protocol === "http:") {
|
|
6330
|
+
const hostname = url.hostname.toLowerCase();
|
|
6331
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
6332
|
+
}
|
|
6333
|
+
return false;
|
|
6334
|
+
}
|
|
6335
|
+
catch {
|
|
6336
|
+
return false;
|
|
6337
|
+
}
|
|
6338
|
+
}, {
|
|
6339
|
+
message: "Must be a valid HTTPS URL or HTTP loopback URL (localhost, 127.0.0.1, [::1])",
|
|
6340
|
+
});
|
|
5133
6341
|
/**
|
|
5134
6342
|
* Zod schema for OAuth configuration validation.
|
|
5135
6343
|
*
|
|
5136
6344
|
* @remarks
|
|
5137
|
-
* All URLs must be valid and use HTTPS in production.
|
|
5138
|
-
*
|
|
6345
|
+
* All URLs must be valid and use HTTPS in production. For local development,
|
|
6346
|
+
* HTTP loopback URLs (localhost, 127.0.0.1, [::1]) are allowed.
|
|
6347
|
+
* The `jwkPrivate` field should contain the private key in JWK (JSON Web Key) format as a string.
|
|
5139
6348
|
*/
|
|
5140
6349
|
const OAuthConfigSchema = zod.z.object({
|
|
5141
6350
|
/**
|
|
5142
6351
|
* URL to the OAuth client metadata JSON document.
|
|
5143
6352
|
* This document describes your application to the authorization server.
|
|
5144
6353
|
*
|
|
6354
|
+
* For local development, you can use `http://localhost/` as a loopback client.
|
|
6355
|
+
*
|
|
5145
6356
|
* @see https://atproto.com/specs/oauth#client-metadata
|
|
5146
6357
|
*/
|
|
5147
|
-
clientId:
|
|
6358
|
+
clientId: urlOrLoopback,
|
|
5148
6359
|
/**
|
|
5149
6360
|
* URL where users are redirected after authentication.
|
|
5150
6361
|
* Must match one of the redirect URIs in your client metadata.
|
|
6362
|
+
*
|
|
6363
|
+
* For local development, you can use HTTP loopback URLs like
|
|
6364
|
+
* `http://127.0.0.1:3000/callback` or `http://localhost:3000/callback`.
|
|
5151
6365
|
*/
|
|
5152
|
-
redirectUri:
|
|
6366
|
+
redirectUri: urlOrLoopback,
|
|
5153
6367
|
/**
|
|
5154
6368
|
* OAuth scopes to request, space-separated.
|
|
5155
6369
|
*
|
|
@@ -5183,8 +6397,11 @@ const OAuthConfigSchema = zod.z.object({
|
|
|
5183
6397
|
/**
|
|
5184
6398
|
* URL to your public JWKS (JSON Web Key Set) endpoint.
|
|
5185
6399
|
* Used by the authorization server to verify your client's signatures.
|
|
6400
|
+
*
|
|
6401
|
+
* For local development, you can serve JWKS from a loopback URL like
|
|
6402
|
+
* `http://127.0.0.1:3000/.well-known/jwks.json`.
|
|
5186
6403
|
*/
|
|
5187
|
-
jwksUri:
|
|
6404
|
+
jwksUri: urlOrLoopback,
|
|
5188
6405
|
/**
|
|
5189
6406
|
* Private JWK (JSON Web Key) as a JSON string.
|
|
5190
6407
|
* Used for signing DPoP proofs and client assertions.
|
|
@@ -5194,28 +6411,64 @@ const OAuthConfigSchema = zod.z.object({
|
|
|
5194
6411
|
* Typically loaded from environment variables or a secrets manager.
|
|
5195
6412
|
*/
|
|
5196
6413
|
jwkPrivate: zod.z.string(),
|
|
6414
|
+
/**
|
|
6415
|
+
* Enable development mode features (optional).
|
|
6416
|
+
*
|
|
6417
|
+
* When true, suppresses warnings about using HTTP loopback URLs.
|
|
6418
|
+
* Should be set to true for local development to reduce console noise.
|
|
6419
|
+
*
|
|
6420
|
+
* @default false
|
|
6421
|
+
*
|
|
6422
|
+
* @example
|
|
6423
|
+
* ```typescript
|
|
6424
|
+
* oauth: {
|
|
6425
|
+
* clientId: "http://localhost/",
|
|
6426
|
+
* redirectUri: "http://127.0.0.1:3000/callback",
|
|
6427
|
+
* // ... other config
|
|
6428
|
+
* developmentMode: true, // Suppress loopback warnings
|
|
6429
|
+
* }
|
|
6430
|
+
* ```
|
|
6431
|
+
*/
|
|
6432
|
+
developmentMode: zod.z.boolean().optional(),
|
|
5197
6433
|
});
|
|
5198
6434
|
/**
|
|
5199
6435
|
* Zod schema for server URL configuration.
|
|
5200
6436
|
*
|
|
5201
6437
|
* @remarks
|
|
5202
6438
|
* At least one server (PDS or SDS) should be configured for the SDK to be useful.
|
|
6439
|
+
* For local development, HTTP loopback URLs are allowed.
|
|
5203
6440
|
*/
|
|
5204
6441
|
const ServerConfigSchema = zod.z.object({
|
|
5205
6442
|
/**
|
|
5206
6443
|
* Personal Data Server URL - the user's own AT Protocol server.
|
|
5207
6444
|
* This is the primary server for user data operations.
|
|
5208
6445
|
*
|
|
5209
|
-
* @example
|
|
6446
|
+
* @example Production
|
|
6447
|
+
* ```typescript
|
|
6448
|
+
* pds: "https://bsky.social"
|
|
6449
|
+
* ```
|
|
6450
|
+
*
|
|
6451
|
+
* @example Local development
|
|
6452
|
+
* ```typescript
|
|
6453
|
+
* pds: "http://localhost:2583"
|
|
6454
|
+
* ```
|
|
5210
6455
|
*/
|
|
5211
|
-
pds:
|
|
6456
|
+
pds: urlOrLoopback.optional(),
|
|
5212
6457
|
/**
|
|
5213
6458
|
* Shared Data Server URL - for collaborative data storage.
|
|
5214
6459
|
* Required for collaborator and organization operations.
|
|
5215
6460
|
*
|
|
5216
|
-
* @example
|
|
6461
|
+
* @example Production
|
|
6462
|
+
* ```typescript
|
|
6463
|
+
* sds: "https://sds.hypercerts.org"
|
|
6464
|
+
* ```
|
|
6465
|
+
*
|
|
6466
|
+
* @example Local development
|
|
6467
|
+
* ```typescript
|
|
6468
|
+
* sds: "http://127.0.0.1:2584"
|
|
6469
|
+
* ```
|
|
5217
6470
|
*/
|
|
5218
|
-
sds:
|
|
6471
|
+
sds: urlOrLoopback.optional(),
|
|
5219
6472
|
});
|
|
5220
6473
|
/**
|
|
5221
6474
|
* Zod schema for timeout configuration.
|
|
@@ -5342,6 +6595,10 @@ class ATProtoSDK {
|
|
|
5342
6595
|
};
|
|
5343
6596
|
this.config = configWithDefaults;
|
|
5344
6597
|
this.logger = config.logger;
|
|
6598
|
+
// Initialize lexicon registry with hypercert lexicons
|
|
6599
|
+
// Filter out undefined lexicons (some may not be exported from lexicon package yet)
|
|
6600
|
+
const validLexicons = HYPERCERT_LEXICONS.filter((lex) => lex !== undefined);
|
|
6601
|
+
this.lexiconRegistry = new LexiconRegistry(validLexicons);
|
|
5345
6602
|
// Initialize OAuth client
|
|
5346
6603
|
this.oauthClient = new OAuthClient(configWithDefaults);
|
|
5347
6604
|
this.logger?.info("ATProto SDK initialized");
|
|
@@ -5625,45 +6882,1160 @@ class ATProtoSDK {
|
|
|
5625
6882
|
}
|
|
5626
6883
|
// Get repository DID (default to session DID)
|
|
5627
6884
|
const repoDid = session.did || session.sub;
|
|
5628
|
-
return new Repository(session, serverUrl, repoDid, isSDS, this.logger);
|
|
6885
|
+
return new Repository(session, serverUrl, repoDid, isSDS, this.logger, this.lexiconRegistry);
|
|
6886
|
+
}
|
|
6887
|
+
/**
|
|
6888
|
+
* Gets the LexiconRegistry instance for managing custom lexicons.
|
|
6889
|
+
*
|
|
6890
|
+
* The registry allows you to register custom lexicon schemas and validate
|
|
6891
|
+
* records against them. All registered lexicons will be automatically
|
|
6892
|
+
* validated during record creation operations.
|
|
6893
|
+
*
|
|
6894
|
+
* @returns The {@link LexiconRegistry} instance
|
|
6895
|
+
*
|
|
6896
|
+
* @example
|
|
6897
|
+
* ```typescript
|
|
6898
|
+
* // Register a custom lexicon
|
|
6899
|
+
* const registry = sdk.getLexiconRegistry();
|
|
6900
|
+
* registry.register({
|
|
6901
|
+
* lexicon: 1,
|
|
6902
|
+
* id: "org.myapp.customRecord",
|
|
6903
|
+
* defs: { ... }
|
|
6904
|
+
* });
|
|
6905
|
+
*
|
|
6906
|
+
* // Check if lexicon is registered
|
|
6907
|
+
* if (registry.isRegistered("org.myapp.customRecord")) {
|
|
6908
|
+
* console.log("Custom lexicon is available");
|
|
6909
|
+
* }
|
|
6910
|
+
* ```
|
|
6911
|
+
*/
|
|
6912
|
+
getLexiconRegistry() {
|
|
6913
|
+
return this.lexiconRegistry;
|
|
6914
|
+
}
|
|
6915
|
+
/**
|
|
6916
|
+
* The configured PDS (Personal Data Server) URL.
|
|
6917
|
+
*
|
|
6918
|
+
* @returns The PDS URL if configured, otherwise `undefined`
|
|
6919
|
+
*/
|
|
6920
|
+
get pdsUrl() {
|
|
6921
|
+
return this.config.servers?.pds;
|
|
6922
|
+
}
|
|
6923
|
+
/**
|
|
6924
|
+
* The configured SDS (Shared Data Server) URL.
|
|
6925
|
+
*
|
|
6926
|
+
* @returns The SDS URL if configured, otherwise `undefined`
|
|
6927
|
+
*/
|
|
6928
|
+
get sdsUrl() {
|
|
6929
|
+
return this.config.servers?.sds;
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
6933
|
+
* Factory function to create an ATProto SDK instance.
|
|
6934
|
+
*
|
|
6935
|
+
* This is a convenience function equivalent to `new ATProtoSDK(config)`.
|
|
6936
|
+
*
|
|
6937
|
+
* @param config - SDK configuration
|
|
6938
|
+
* @returns A new {@link ATProtoSDK} instance
|
|
6939
|
+
*
|
|
6940
|
+
* @example
|
|
6941
|
+
* ```typescript
|
|
6942
|
+
* import { createATProtoSDK } from "@hypercerts-org/sdk";
|
|
6943
|
+
*
|
|
6944
|
+
* const sdk = createATProtoSDK({
|
|
6945
|
+
* oauth: { ... },
|
|
6946
|
+
* servers: { pds: "https://bsky.social" },
|
|
6947
|
+
* });
|
|
6948
|
+
* ```
|
|
6949
|
+
*/
|
|
6950
|
+
function createATProtoSDK(config) {
|
|
6951
|
+
return new ATProtoSDK(config);
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
/**
|
|
6955
|
+
* BaseOperations - Abstract base class for custom lexicon operations.
|
|
6956
|
+
*
|
|
6957
|
+
* This module provides a foundation for building domain-specific operation
|
|
6958
|
+
* classes that work with custom lexicons. It handles validation, record
|
|
6959
|
+
* creation, and provides utilities for working with AT Protocol records.
|
|
6960
|
+
*
|
|
6961
|
+
* @packageDocumentation
|
|
6962
|
+
*/
|
|
6963
|
+
/**
|
|
6964
|
+
* Abstract base class for creating custom lexicon operation classes.
|
|
6965
|
+
*
|
|
6966
|
+
* Extend this class to build domain-specific operations for your custom
|
|
6967
|
+
* lexicons. The base class provides:
|
|
6968
|
+
*
|
|
6969
|
+
* - Automatic validation against registered lexicon schemas
|
|
6970
|
+
* - Helper methods for creating and updating records
|
|
6971
|
+
* - Utilities for building strongRefs and AT-URIs
|
|
6972
|
+
* - Error handling and logging support
|
|
6973
|
+
*
|
|
6974
|
+
* @typeParam TParams - Type of parameters accepted by the create() method
|
|
6975
|
+
* @typeParam TResult - Type of result returned by the create() method
|
|
6976
|
+
*
|
|
6977
|
+
* @remarks
|
|
6978
|
+
* This class is designed to be extended by developers creating custom
|
|
6979
|
+
* operation classes for their own lexicons. It follows the same patterns
|
|
6980
|
+
* as the built-in hypercert operations.
|
|
6981
|
+
*
|
|
6982
|
+
* @example Basic usage
|
|
6983
|
+
* ```typescript
|
|
6984
|
+
* import { BaseOperations } from "@hypercerts-org/sdk-core";
|
|
6985
|
+
*
|
|
6986
|
+
* interface EvaluationParams {
|
|
6987
|
+
* subjectUri: string;
|
|
6988
|
+
* subjectCid: string;
|
|
6989
|
+
* score: number;
|
|
6990
|
+
* methodology?: string;
|
|
6991
|
+
* }
|
|
6992
|
+
*
|
|
6993
|
+
* interface EvaluationResult {
|
|
6994
|
+
* uri: string;
|
|
6995
|
+
* cid: string;
|
|
6996
|
+
* record: MyEvaluation;
|
|
6997
|
+
* }
|
|
6998
|
+
*
|
|
6999
|
+
* class EvaluationOperations extends BaseOperations<EvaluationParams, EvaluationResult> {
|
|
7000
|
+
* async create(params: EvaluationParams): Promise<EvaluationResult> {
|
|
7001
|
+
* const record = {
|
|
7002
|
+
* $type: "org.myapp.evaluation",
|
|
7003
|
+
* subject: this.createStrongRef(params.subjectUri, params.subjectCid),
|
|
7004
|
+
* score: params.score,
|
|
7005
|
+
* methodology: params.methodology,
|
|
7006
|
+
* createdAt: new Date().toISOString(),
|
|
7007
|
+
* };
|
|
7008
|
+
*
|
|
7009
|
+
* const { uri, cid } = await this.validateAndCreate("org.myapp.evaluation", record);
|
|
7010
|
+
* return { uri, cid, record };
|
|
7011
|
+
* }
|
|
7012
|
+
* }
|
|
7013
|
+
* ```
|
|
7014
|
+
*
|
|
7015
|
+
* @example With validation and error handling
|
|
7016
|
+
* ```typescript
|
|
7017
|
+
* class ProjectOperations extends BaseOperations<CreateProjectParams, ProjectResult> {
|
|
7018
|
+
* async create(params: CreateProjectParams): Promise<ProjectResult> {
|
|
7019
|
+
* // Validate input parameters
|
|
7020
|
+
* if (!params.title || params.title.trim().length === 0) {
|
|
7021
|
+
* throw new ValidationError("Project title cannot be empty");
|
|
7022
|
+
* }
|
|
7023
|
+
*
|
|
7024
|
+
* const record = {
|
|
7025
|
+
* $type: "org.myapp.project",
|
|
7026
|
+
* title: params.title,
|
|
7027
|
+
* description: params.description,
|
|
7028
|
+
* createdAt: new Date().toISOString(),
|
|
7029
|
+
* };
|
|
7030
|
+
*
|
|
7031
|
+
* try {
|
|
7032
|
+
* const { uri, cid } = await this.validateAndCreate("org.myapp.project", record);
|
|
7033
|
+
* this.logger?.info(`Created project: ${uri}`);
|
|
7034
|
+
* return { uri, cid, record };
|
|
7035
|
+
* } catch (error) {
|
|
7036
|
+
* this.logger?.error(`Failed to create project: ${error}`);
|
|
7037
|
+
* throw error;
|
|
7038
|
+
* }
|
|
7039
|
+
* }
|
|
7040
|
+
* }
|
|
7041
|
+
* ```
|
|
7042
|
+
*/
|
|
7043
|
+
class BaseOperations {
|
|
7044
|
+
/**
|
|
7045
|
+
* Creates a new BaseOperations instance.
|
|
7046
|
+
*
|
|
7047
|
+
* @param agent - AT Protocol Agent for making API calls
|
|
7048
|
+
* @param repoDid - DID of the repository to operate on
|
|
7049
|
+
* @param lexiconRegistry - Registry for validating records against lexicon schemas
|
|
7050
|
+
* @param logger - Optional logger for debugging and monitoring
|
|
7051
|
+
*
|
|
7052
|
+
* @internal
|
|
7053
|
+
*/
|
|
7054
|
+
constructor(agent, repoDid, lexiconRegistry, logger) {
|
|
7055
|
+
this.agent = agent;
|
|
7056
|
+
this.repoDid = repoDid;
|
|
7057
|
+
this.lexiconRegistry = lexiconRegistry;
|
|
7058
|
+
this.logger = logger;
|
|
7059
|
+
}
|
|
7060
|
+
/**
|
|
7061
|
+
* Validates a record against its lexicon schema and creates it in the repository.
|
|
7062
|
+
*
|
|
7063
|
+
* This method performs the following steps:
|
|
7064
|
+
* 1. Validates the record against the registered lexicon schema
|
|
7065
|
+
* 2. Throws ValidationError if validation fails
|
|
7066
|
+
* 3. Creates the record using the AT Protocol Agent
|
|
7067
|
+
* 4. Returns the created record's URI and CID
|
|
7068
|
+
*
|
|
7069
|
+
* @param collection - NSID of the collection (e.g., "org.myapp.customRecord")
|
|
7070
|
+
* @param record - Record data conforming to the collection's lexicon schema
|
|
7071
|
+
* @param rkey - Optional record key. If not provided, a TID is auto-generated
|
|
7072
|
+
* @returns Promise resolving to the created record's URI and CID
|
|
7073
|
+
* @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
|
|
7074
|
+
* @throws {@link NetworkError} if the API request fails
|
|
7075
|
+
*
|
|
7076
|
+
* @example
|
|
7077
|
+
* ```typescript
|
|
7078
|
+
* const record = {
|
|
7079
|
+
* $type: "org.myapp.evaluation",
|
|
7080
|
+
* subject: { uri: "at://...", cid: "bafyrei..." },
|
|
7081
|
+
* score: 85,
|
|
7082
|
+
* createdAt: new Date().toISOString(),
|
|
7083
|
+
* };
|
|
7084
|
+
*
|
|
7085
|
+
* const { uri, cid } = await this.validateAndCreate("org.myapp.evaluation", record);
|
|
7086
|
+
* ```
|
|
7087
|
+
*/
|
|
7088
|
+
async validateAndCreate(collection, record, rkey) {
|
|
7089
|
+
// Validate record against registered lexicon
|
|
7090
|
+
if (this.lexiconRegistry.isRegistered(collection)) {
|
|
7091
|
+
const validation = this.lexiconRegistry.validate(collection, record);
|
|
7092
|
+
if (!validation.valid) {
|
|
7093
|
+
throw new ValidationError(`Invalid ${collection}: ${validation.error}`);
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
try {
|
|
7097
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
7098
|
+
repo: this.repoDid,
|
|
7099
|
+
collection,
|
|
7100
|
+
record: record,
|
|
7101
|
+
rkey,
|
|
7102
|
+
});
|
|
7103
|
+
if (!result.success) {
|
|
7104
|
+
throw new NetworkError("Failed to create record");
|
|
7105
|
+
}
|
|
7106
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
7107
|
+
}
|
|
7108
|
+
catch (error) {
|
|
7109
|
+
if (error instanceof ValidationError || error instanceof NetworkError) {
|
|
7110
|
+
throw error;
|
|
7111
|
+
}
|
|
7112
|
+
throw new NetworkError(`Failed to create ${collection}: ${error instanceof Error ? error.message : "Unknown error"}`, error);
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
/**
|
|
7116
|
+
* Validates a record against its lexicon schema and updates it in the repository.
|
|
7117
|
+
*
|
|
7118
|
+
* This method performs the following steps:
|
|
7119
|
+
* 1. Validates the record against the registered lexicon schema
|
|
7120
|
+
* 2. Throws ValidationError if validation fails
|
|
7121
|
+
* 3. Updates the record using the AT Protocol Agent
|
|
7122
|
+
* 4. Returns the updated record's URI and new CID
|
|
7123
|
+
*
|
|
7124
|
+
* @param collection - NSID of the collection
|
|
7125
|
+
* @param rkey - Record key (the last segment of the AT-URI)
|
|
7126
|
+
* @param record - New record data (completely replaces existing record)
|
|
7127
|
+
* @returns Promise resolving to the updated record's URI and new CID
|
|
7128
|
+
* @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
|
|
7129
|
+
* @throws {@link NetworkError} if the API request fails
|
|
7130
|
+
*
|
|
7131
|
+
* @remarks
|
|
7132
|
+
* This is a full replacement operation, not a partial update.
|
|
7133
|
+
*
|
|
7134
|
+
* @example
|
|
7135
|
+
* ```typescript
|
|
7136
|
+
* const updatedRecord = {
|
|
7137
|
+
* $type: "org.myapp.evaluation",
|
|
7138
|
+
* subject: { uri: "at://...", cid: "bafyrei..." },
|
|
7139
|
+
* score: 90, // Updated score
|
|
7140
|
+
* createdAt: existingRecord.createdAt,
|
|
7141
|
+
* };
|
|
7142
|
+
*
|
|
7143
|
+
* const { uri, cid } = await this.validateAndUpdate(
|
|
7144
|
+
* "org.myapp.evaluation",
|
|
7145
|
+
* "abc123",
|
|
7146
|
+
* updatedRecord
|
|
7147
|
+
* );
|
|
7148
|
+
* ```
|
|
7149
|
+
*/
|
|
7150
|
+
async validateAndUpdate(collection, rkey, record) {
|
|
7151
|
+
// Validate record against registered lexicon
|
|
7152
|
+
if (this.lexiconRegistry.isRegistered(collection)) {
|
|
7153
|
+
const validation = this.lexiconRegistry.validate(collection, record);
|
|
7154
|
+
if (!validation.valid) {
|
|
7155
|
+
throw new ValidationError(`Invalid ${collection}: ${validation.error}`);
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
try {
|
|
7159
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
7160
|
+
repo: this.repoDid,
|
|
7161
|
+
collection,
|
|
7162
|
+
rkey,
|
|
7163
|
+
record: record,
|
|
7164
|
+
});
|
|
7165
|
+
if (!result.success) {
|
|
7166
|
+
throw new NetworkError("Failed to update record");
|
|
7167
|
+
}
|
|
7168
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
7169
|
+
}
|
|
7170
|
+
catch (error) {
|
|
7171
|
+
if (error instanceof ValidationError || error instanceof NetworkError) {
|
|
7172
|
+
throw error;
|
|
7173
|
+
}
|
|
7174
|
+
throw new NetworkError(`Failed to update ${collection}: ${error instanceof Error ? error.message : "Unknown error"}`, error);
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
/**
|
|
7178
|
+
* Creates a strongRef object from a URI and CID.
|
|
7179
|
+
*
|
|
7180
|
+
* StrongRefs are used in AT Protocol to reference specific versions
|
|
7181
|
+
* of records. They ensure that references point to an exact record
|
|
7182
|
+
* version, not just the latest version.
|
|
7183
|
+
*
|
|
7184
|
+
* @param uri - AT-URI of the record (e.g., "at://did:plc:abc/collection/rkey")
|
|
7185
|
+
* @param cid - Content Identifier (CID) of the record
|
|
7186
|
+
* @returns StrongRef object with uri and cid properties
|
|
7187
|
+
*
|
|
7188
|
+
* @example
|
|
7189
|
+
* ```typescript
|
|
7190
|
+
* const hypercertRef = this.createStrongRef(
|
|
7191
|
+
* "at://did:plc:abc123/org.hypercerts.claim.activity/xyz789",
|
|
7192
|
+
* "bafyreiabc123..."
|
|
7193
|
+
* );
|
|
7194
|
+
*
|
|
7195
|
+
* const evaluation = {
|
|
7196
|
+
* $type: "org.myapp.evaluation",
|
|
7197
|
+
* subject: hypercertRef, // Reference to specific hypercert version
|
|
7198
|
+
* score: 85,
|
|
7199
|
+
* createdAt: new Date().toISOString(),
|
|
7200
|
+
* };
|
|
7201
|
+
* ```
|
|
7202
|
+
*/
|
|
7203
|
+
createStrongRef(uri, cid) {
|
|
7204
|
+
return { uri, cid };
|
|
7205
|
+
}
|
|
7206
|
+
/**
|
|
7207
|
+
* Creates a strongRef from a CreateResult or UpdateResult.
|
|
7208
|
+
*
|
|
7209
|
+
* This is a convenience method for creating strongRefs from the
|
|
7210
|
+
* results of create or update operations.
|
|
7211
|
+
*
|
|
7212
|
+
* @param result - Result from a create or update operation
|
|
7213
|
+
* @returns StrongRef object with uri and cid properties
|
|
7214
|
+
*
|
|
7215
|
+
* @example
|
|
7216
|
+
* ```typescript
|
|
7217
|
+
* // Create a project
|
|
7218
|
+
* const projectResult = await this.validateAndCreate("org.myapp.project", projectRecord);
|
|
7219
|
+
*
|
|
7220
|
+
* // Create a task that references the project
|
|
7221
|
+
* const taskRecord = {
|
|
7222
|
+
* $type: "org.myapp.task",
|
|
7223
|
+
* project: this.createStrongRefFromResult(projectResult),
|
|
7224
|
+
* title: "Implement feature",
|
|
7225
|
+
* createdAt: new Date().toISOString(),
|
|
7226
|
+
* };
|
|
7227
|
+
* ```
|
|
7228
|
+
*/
|
|
7229
|
+
createStrongRefFromResult(result) {
|
|
7230
|
+
return { uri: result.uri, cid: result.cid };
|
|
7231
|
+
}
|
|
7232
|
+
/**
|
|
7233
|
+
* Parses an AT-URI to extract its components.
|
|
7234
|
+
*
|
|
7235
|
+
* AT-URIs follow the format: `at://{did}/{collection}/{rkey}`
|
|
7236
|
+
*
|
|
7237
|
+
* @param uri - AT-URI to parse
|
|
7238
|
+
* @returns Object containing did, collection, and rkey
|
|
7239
|
+
* @throws Error if the URI format is invalid
|
|
7240
|
+
*
|
|
7241
|
+
* @example
|
|
7242
|
+
* ```typescript
|
|
7243
|
+
* const { did, collection, rkey } = this.parseAtUri(
|
|
7244
|
+
* "at://did:plc:abc123/org.hypercerts.claim.activity/xyz789"
|
|
7245
|
+
* );
|
|
7246
|
+
* // did: "did:plc:abc123"
|
|
7247
|
+
* // collection: "org.hypercerts.claim.activity"
|
|
7248
|
+
* // rkey: "xyz789"
|
|
7249
|
+
* ```
|
|
7250
|
+
*/
|
|
7251
|
+
parseAtUri(uri) {
|
|
7252
|
+
if (!uri.startsWith("at://")) {
|
|
7253
|
+
throw new Error(`Invalid AT-URI format: ${uri}`);
|
|
7254
|
+
}
|
|
7255
|
+
const parts = uri.slice(5).split("/"); // Remove "at://" and split
|
|
7256
|
+
if (parts.length !== 3) {
|
|
7257
|
+
throw new Error(`Invalid AT-URI format: ${uri}`);
|
|
7258
|
+
}
|
|
7259
|
+
return {
|
|
7260
|
+
did: parts[0],
|
|
7261
|
+
collection: parts[1],
|
|
7262
|
+
rkey: parts[2],
|
|
7263
|
+
};
|
|
7264
|
+
}
|
|
7265
|
+
/**
|
|
7266
|
+
* Builds an AT-URI from its components.
|
|
7267
|
+
*
|
|
7268
|
+
* @param did - DID of the repository
|
|
7269
|
+
* @param collection - NSID of the collection
|
|
7270
|
+
* @param rkey - Record key (typically a TID)
|
|
7271
|
+
* @returns Complete AT-URI string
|
|
7272
|
+
*
|
|
7273
|
+
* @example
|
|
7274
|
+
* ```typescript
|
|
7275
|
+
* const uri = this.buildAtUri(
|
|
7276
|
+
* "did:plc:abc123",
|
|
7277
|
+
* "org.myapp.evaluation",
|
|
7278
|
+
* "xyz789"
|
|
7279
|
+
* );
|
|
7280
|
+
* // Returns: "at://did:plc:abc123/org.myapp.evaluation/xyz789"
|
|
7281
|
+
* ```
|
|
7282
|
+
*/
|
|
7283
|
+
buildAtUri(did, collection, rkey) {
|
|
7284
|
+
return `at://${did}/${collection}/${rkey}`;
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
/**
|
|
7289
|
+
* Lexicon Development Utilities - AT-URI and StrongRef Helpers
|
|
7290
|
+
*
|
|
7291
|
+
* This module provides utilities for working with AT Protocol URIs and strongRefs
|
|
7292
|
+
* when building custom lexicons. These tools help developers create type-safe
|
|
7293
|
+
* references between records.
|
|
7294
|
+
*
|
|
7295
|
+
* @packageDocumentation
|
|
7296
|
+
*/
|
|
7297
|
+
/**
|
|
7298
|
+
* Parse an AT-URI into its component parts.
|
|
7299
|
+
*
|
|
7300
|
+
* Extracts the DID, collection NSID, and record key from an AT-URI string.
|
|
7301
|
+
* AT-URIs follow the format: `at://{did}/{collection}/{rkey}`
|
|
7302
|
+
*
|
|
7303
|
+
* @param uri - The AT-URI to parse
|
|
7304
|
+
* @returns The components of the URI
|
|
7305
|
+
* @throws {Error} If the URI format is invalid
|
|
7306
|
+
*
|
|
7307
|
+
* @example
|
|
7308
|
+
* ```typescript
|
|
7309
|
+
* const components = parseAtUri("at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a");
|
|
7310
|
+
* console.log(components);
|
|
7311
|
+
* // {
|
|
7312
|
+
* // did: "did:plc:abc123",
|
|
7313
|
+
* // collection: "org.hypercerts.claim.activity",
|
|
7314
|
+
* // rkey: "3km2vj4kfqp2a"
|
|
7315
|
+
* // }
|
|
7316
|
+
* ```
|
|
7317
|
+
*/
|
|
7318
|
+
function parseAtUri(uri) {
|
|
7319
|
+
if (!uri.startsWith("at://")) {
|
|
7320
|
+
throw new Error(`Invalid AT-URI format: must start with "at://", got "${uri}"`);
|
|
7321
|
+
}
|
|
7322
|
+
const withoutProtocol = uri.slice(5); // Remove "at://"
|
|
7323
|
+
const parts = withoutProtocol.split("/");
|
|
7324
|
+
if (parts.length !== 3) {
|
|
7325
|
+
throw new Error(`Invalid AT-URI format: expected "at://{did}/{collection}/{rkey}", got "${uri}"`);
|
|
7326
|
+
}
|
|
7327
|
+
const [did, collection, rkey] = parts;
|
|
7328
|
+
if (!did || !collection || !rkey) {
|
|
7329
|
+
throw new Error(`Invalid AT-URI format: all components must be non-empty, got "${uri}"`);
|
|
7330
|
+
}
|
|
7331
|
+
return { did, collection, rkey };
|
|
7332
|
+
}
|
|
7333
|
+
/**
|
|
7334
|
+
* Build an AT-URI from its component parts.
|
|
7335
|
+
*
|
|
7336
|
+
* Constructs a valid AT-URI string from a DID, collection NSID, and record key.
|
|
7337
|
+
* The resulting URI follows the format: `at://{did}/{collection}/{rkey}`
|
|
7338
|
+
*
|
|
7339
|
+
* @param did - The repository owner's DID
|
|
7340
|
+
* @param collection - The collection NSID (lexicon identifier)
|
|
7341
|
+
* @param rkey - The record key (TID or custom string)
|
|
7342
|
+
* @returns The complete AT-URI
|
|
7343
|
+
*
|
|
7344
|
+
* @example
|
|
7345
|
+
* ```typescript
|
|
7346
|
+
* const uri = buildAtUri(
|
|
7347
|
+
* "did:plc:abc123",
|
|
7348
|
+
* "org.hypercerts.claim.activity",
|
|
7349
|
+
* "3km2vj4kfqp2a"
|
|
7350
|
+
* );
|
|
7351
|
+
* console.log(uri); // "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a"
|
|
7352
|
+
* ```
|
|
7353
|
+
*/
|
|
7354
|
+
function buildAtUri(did, collection, rkey) {
|
|
7355
|
+
if (!did || !collection || !rkey) {
|
|
7356
|
+
throw new Error("All AT-URI components (did, collection, rkey) must be non-empty");
|
|
7357
|
+
}
|
|
7358
|
+
return `at://${did}/${collection}/${rkey}`;
|
|
7359
|
+
}
|
|
7360
|
+
/**
|
|
7361
|
+
* Extract the record key (TID or custom key) from an AT-URI.
|
|
7362
|
+
*
|
|
7363
|
+
* Returns the last component of the AT-URI, which is the record key.
|
|
7364
|
+
* This is equivalent to `parseAtUri(uri).rkey` but more efficient.
|
|
7365
|
+
*
|
|
7366
|
+
* @param uri - The AT-URI to extract from
|
|
7367
|
+
* @returns The record key (TID or custom string)
|
|
7368
|
+
* @throws {Error} If the URI format is invalid
|
|
7369
|
+
*
|
|
7370
|
+
* @example
|
|
7371
|
+
* ```typescript
|
|
7372
|
+
* const rkey = extractRkeyFromUri("at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a");
|
|
7373
|
+
* console.log(rkey); // "3km2vj4kfqp2a"
|
|
7374
|
+
* ```
|
|
7375
|
+
*/
|
|
7376
|
+
function extractRkeyFromUri(uri) {
|
|
7377
|
+
const { rkey } = parseAtUri(uri);
|
|
7378
|
+
return rkey;
|
|
7379
|
+
}
|
|
7380
|
+
/**
|
|
7381
|
+
* Check if a string is a valid AT-URI format.
|
|
7382
|
+
*
|
|
7383
|
+
* Validates that the string follows the AT-URI format without throwing errors.
|
|
7384
|
+
* This is useful for input validation before parsing.
|
|
7385
|
+
*
|
|
7386
|
+
* @param uri - The string to validate
|
|
7387
|
+
* @returns True if the string is a valid AT-URI, false otherwise
|
|
7388
|
+
*
|
|
7389
|
+
* @example
|
|
7390
|
+
* ```typescript
|
|
7391
|
+
* if (isValidAtUri(userInput)) {
|
|
7392
|
+
* const components = parseAtUri(userInput);
|
|
7393
|
+
* // ... use components
|
|
7394
|
+
* } else {
|
|
7395
|
+
* console.error("Invalid AT-URI");
|
|
7396
|
+
* }
|
|
7397
|
+
* ```
|
|
7398
|
+
*/
|
|
7399
|
+
function isValidAtUri(uri) {
|
|
7400
|
+
try {
|
|
7401
|
+
parseAtUri(uri);
|
|
7402
|
+
return true;
|
|
7403
|
+
}
|
|
7404
|
+
catch {
|
|
7405
|
+
return false;
|
|
5629
7406
|
}
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
7407
|
+
}
|
|
7408
|
+
/**
|
|
7409
|
+
* Create a strongRef from a URI and CID.
|
|
7410
|
+
*
|
|
7411
|
+
* StrongRefs are the canonical way to reference specific versions of records
|
|
7412
|
+
* in AT Protocol. They combine an AT-URI (which identifies the record) with
|
|
7413
|
+
* a CID (which identifies the specific version).
|
|
7414
|
+
*
|
|
7415
|
+
* @param uri - The AT-URI of the record
|
|
7416
|
+
* @param cid - The CID (Content Identifier) of the record version
|
|
7417
|
+
* @returns A strongRef object
|
|
7418
|
+
*
|
|
7419
|
+
* @example
|
|
7420
|
+
* ```typescript
|
|
7421
|
+
* const ref = createStrongRef(
|
|
7422
|
+
* "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a",
|
|
7423
|
+
* "bafyreiabc123..."
|
|
7424
|
+
* );
|
|
7425
|
+
* console.log(ref);
|
|
7426
|
+
* // {
|
|
7427
|
+
* // uri: "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a",
|
|
7428
|
+
* // cid: "bafyreiabc123..."
|
|
7429
|
+
* // }
|
|
7430
|
+
* ```
|
|
7431
|
+
*/
|
|
7432
|
+
function createStrongRef(uri, cid) {
|
|
7433
|
+
if (!uri || !cid) {
|
|
7434
|
+
throw new Error("Both uri and cid are required to create a strongRef");
|
|
5637
7435
|
}
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
7436
|
+
return { uri, cid };
|
|
7437
|
+
}
|
|
7438
|
+
/**
|
|
7439
|
+
* Create a strongRef from a CreateResult or UpdateResult.
|
|
7440
|
+
*
|
|
7441
|
+
* This is a convenience function that extracts the URI and CID from
|
|
7442
|
+
* the result of a record creation or update operation.
|
|
7443
|
+
*
|
|
7444
|
+
* @param result - The result from creating or updating a record
|
|
7445
|
+
* @returns A strongRef object
|
|
7446
|
+
*
|
|
7447
|
+
* @example
|
|
7448
|
+
* ```typescript
|
|
7449
|
+
* const hypercert = await repo.hypercerts.create({
|
|
7450
|
+
* title: "Climate Research",
|
|
7451
|
+
* // ... other params
|
|
7452
|
+
* });
|
|
7453
|
+
*
|
|
7454
|
+
* const ref = createStrongRefFromResult(hypercert);
|
|
7455
|
+
* // Now use ref in another record to reference this hypercert
|
|
7456
|
+
* ```
|
|
7457
|
+
*/
|
|
7458
|
+
function createStrongRefFromResult(result) {
|
|
7459
|
+
return createStrongRef(result.uri, result.cid);
|
|
7460
|
+
}
|
|
7461
|
+
/**
|
|
7462
|
+
* Validate that an object is a valid strongRef.
|
|
7463
|
+
*
|
|
7464
|
+
* Checks that the object has the required `uri` and `cid` properties
|
|
7465
|
+
* and that they are non-empty strings.
|
|
7466
|
+
*
|
|
7467
|
+
* @param ref - The object to validate
|
|
7468
|
+
* @returns True if the object is a valid strongRef, false otherwise
|
|
7469
|
+
*
|
|
7470
|
+
* @example
|
|
7471
|
+
* ```typescript
|
|
7472
|
+
* const maybeRef = { uri: "at://...", cid: "bafyrei..." };
|
|
7473
|
+
* if (validateStrongRef(maybeRef)) {
|
|
7474
|
+
* // Safe to use as strongRef
|
|
7475
|
+
* record.subject = maybeRef;
|
|
7476
|
+
* }
|
|
7477
|
+
* ```
|
|
7478
|
+
*/
|
|
7479
|
+
function validateStrongRef(ref) {
|
|
7480
|
+
if (!ref || typeof ref !== "object") {
|
|
7481
|
+
return false;
|
|
5645
7482
|
}
|
|
7483
|
+
const obj = ref;
|
|
7484
|
+
return typeof obj.uri === "string" && obj.uri.length > 0 && typeof obj.cid === "string" && obj.cid.length > 0;
|
|
5646
7485
|
}
|
|
5647
7486
|
/**
|
|
5648
|
-
*
|
|
7487
|
+
* Type guard to check if a value is a strongRef.
|
|
5649
7488
|
*
|
|
5650
|
-
* This is
|
|
7489
|
+
* This is an alias for `validateStrongRef` that provides better semantics
|
|
7490
|
+
* for type narrowing in TypeScript.
|
|
5651
7491
|
*
|
|
5652
|
-
* @param
|
|
5653
|
-
* @returns
|
|
7492
|
+
* @param value - The value to check
|
|
7493
|
+
* @returns True if the value is a strongRef, false otherwise
|
|
5654
7494
|
*
|
|
5655
7495
|
* @example
|
|
5656
7496
|
* ```typescript
|
|
5657
|
-
*
|
|
7497
|
+
* function processReference(ref: unknown) {
|
|
7498
|
+
* if (isStrongRef(ref)) {
|
|
7499
|
+
* // TypeScript knows ref is StrongRef here
|
|
7500
|
+
* console.log(ref.uri);
|
|
7501
|
+
* }
|
|
7502
|
+
* }
|
|
7503
|
+
* ```
|
|
7504
|
+
*/
|
|
7505
|
+
function isStrongRef(value) {
|
|
7506
|
+
return validateStrongRef(value);
|
|
7507
|
+
}
|
|
7508
|
+
|
|
7509
|
+
/**
|
|
7510
|
+
* Lexicon Builder Utilities
|
|
5658
7511
|
*
|
|
5659
|
-
*
|
|
5660
|
-
*
|
|
5661
|
-
*
|
|
7512
|
+
* This module provides utilities for constructing lexicon JSON schemas programmatically.
|
|
7513
|
+
* These builders help developers create valid AT Protocol lexicons with proper structure
|
|
7514
|
+
* and type-safe field definitions.
|
|
7515
|
+
*
|
|
7516
|
+
* @packageDocumentation
|
|
7517
|
+
*/
|
|
7518
|
+
/**
|
|
7519
|
+
* Create a string field definition.
|
|
7520
|
+
*
|
|
7521
|
+
* @param options - String field options
|
|
7522
|
+
* @returns A lexicon string field definition
|
|
7523
|
+
*
|
|
7524
|
+
* @example
|
|
7525
|
+
* ```typescript
|
|
7526
|
+
* const titleField = createStringField({
|
|
7527
|
+
* description: "Title of the item",
|
|
7528
|
+
* minLength: 1,
|
|
7529
|
+
* maxLength: 200,
|
|
5662
7530
|
* });
|
|
5663
7531
|
* ```
|
|
5664
7532
|
*/
|
|
5665
|
-
function
|
|
5666
|
-
return
|
|
7533
|
+
function createStringField(options = {}) {
|
|
7534
|
+
return { type: "string", ...options };
|
|
7535
|
+
}
|
|
7536
|
+
/**
|
|
7537
|
+
* Create an integer field definition.
|
|
7538
|
+
*
|
|
7539
|
+
* @param options - Integer field options
|
|
7540
|
+
* @returns A lexicon integer field definition
|
|
7541
|
+
*
|
|
7542
|
+
* @example
|
|
7543
|
+
* ```typescript
|
|
7544
|
+
* const scoreField = createIntegerField({
|
|
7545
|
+
* description: "Score from 0 to 100",
|
|
7546
|
+
* minimum: 0,
|
|
7547
|
+
* maximum: 100,
|
|
7548
|
+
* });
|
|
7549
|
+
* ```
|
|
7550
|
+
*/
|
|
7551
|
+
function createIntegerField(options = {}) {
|
|
7552
|
+
return { type: "integer", ...options };
|
|
7553
|
+
}
|
|
7554
|
+
/**
|
|
7555
|
+
* Create a number field definition.
|
|
7556
|
+
*
|
|
7557
|
+
* @param options - Number field options
|
|
7558
|
+
* @returns A lexicon number field definition
|
|
7559
|
+
*
|
|
7560
|
+
* @example
|
|
7561
|
+
* ```typescript
|
|
7562
|
+
* const weightField = createNumberField({
|
|
7563
|
+
* description: "Weight as decimal",
|
|
7564
|
+
* minimum: 0,
|
|
7565
|
+
* maximum: 1,
|
|
7566
|
+
* });
|
|
7567
|
+
* ```
|
|
7568
|
+
*/
|
|
7569
|
+
function createNumberField(options = {}) {
|
|
7570
|
+
return { type: "number", ...options };
|
|
7571
|
+
}
|
|
7572
|
+
/**
|
|
7573
|
+
* Create a boolean field definition.
|
|
7574
|
+
*
|
|
7575
|
+
* @param options - Boolean field options
|
|
7576
|
+
* @returns A lexicon boolean field definition
|
|
7577
|
+
*
|
|
7578
|
+
* @example
|
|
7579
|
+
* ```typescript
|
|
7580
|
+
* const verifiedField = createBooleanField({
|
|
7581
|
+
* description: "Whether the item is verified",
|
|
7582
|
+
* default: false,
|
|
7583
|
+
* });
|
|
7584
|
+
* ```
|
|
7585
|
+
*/
|
|
7586
|
+
function createBooleanField(options = {}) {
|
|
7587
|
+
return { type: "boolean", ...options };
|
|
7588
|
+
}
|
|
7589
|
+
/**
|
|
7590
|
+
* Create a strongRef field definition.
|
|
7591
|
+
*
|
|
7592
|
+
* StrongRefs are the standard way to reference other records in AT Protocol.
|
|
7593
|
+
* They contain both the AT-URI and CID of the referenced record.
|
|
7594
|
+
*
|
|
7595
|
+
* @param options - Reference field options
|
|
7596
|
+
* @returns A lexicon reference field definition
|
|
7597
|
+
*
|
|
7598
|
+
* @example
|
|
7599
|
+
* ```typescript
|
|
7600
|
+
* const subjectField = createStrongRefField({
|
|
7601
|
+
* description: "The hypercert being evaluated",
|
|
7602
|
+
* });
|
|
7603
|
+
* ```
|
|
7604
|
+
*/
|
|
7605
|
+
function createStrongRefField(options = {}) {
|
|
7606
|
+
return {
|
|
7607
|
+
type: "ref",
|
|
7608
|
+
ref: options.ref || "com.atproto.repo.strongRef",
|
|
7609
|
+
description: options.description,
|
|
7610
|
+
};
|
|
7611
|
+
}
|
|
7612
|
+
/**
|
|
7613
|
+
* Create an array field definition.
|
|
7614
|
+
*
|
|
7615
|
+
* @param itemType - The type of items in the array
|
|
7616
|
+
* @param options - Array field options
|
|
7617
|
+
* @returns A lexicon array field definition
|
|
7618
|
+
*
|
|
7619
|
+
* @example
|
|
7620
|
+
* ```typescript
|
|
7621
|
+
* const tagsField = createArrayField(
|
|
7622
|
+
* createStringField({ maxLength: 50 }),
|
|
7623
|
+
* {
|
|
7624
|
+
* description: "List of tags",
|
|
7625
|
+
* minLength: 1,
|
|
7626
|
+
* maxLength: 10,
|
|
7627
|
+
* }
|
|
7628
|
+
* );
|
|
7629
|
+
* ```
|
|
7630
|
+
*/
|
|
7631
|
+
function createArrayField(itemType, options = {}) {
|
|
7632
|
+
return {
|
|
7633
|
+
type: "array",
|
|
7634
|
+
items: itemType,
|
|
7635
|
+
...options,
|
|
7636
|
+
};
|
|
7637
|
+
}
|
|
7638
|
+
/**
|
|
7639
|
+
* Create an object field definition.
|
|
7640
|
+
*
|
|
7641
|
+
* @param options - Object field options
|
|
7642
|
+
* @returns A lexicon object field definition
|
|
7643
|
+
*
|
|
7644
|
+
* @example
|
|
7645
|
+
* ```typescript
|
|
7646
|
+
* const metadataField = createObjectField({
|
|
7647
|
+
* description: "Additional metadata",
|
|
7648
|
+
* properties: {
|
|
7649
|
+
* author: createStringField(),
|
|
7650
|
+
* version: createIntegerField(),
|
|
7651
|
+
* },
|
|
7652
|
+
* required: ["author"],
|
|
7653
|
+
* });
|
|
7654
|
+
* ```
|
|
7655
|
+
*/
|
|
7656
|
+
function createObjectField(options = {}) {
|
|
7657
|
+
return { type: "object", ...options };
|
|
7658
|
+
}
|
|
7659
|
+
/**
|
|
7660
|
+
* Create a blob field definition.
|
|
7661
|
+
*
|
|
7662
|
+
* @param options - Blob field options
|
|
7663
|
+
* @returns A lexicon blob field definition
|
|
7664
|
+
*
|
|
7665
|
+
* @example
|
|
7666
|
+
* ```typescript
|
|
7667
|
+
* const imageField = createBlobField({
|
|
7668
|
+
* description: "Profile image",
|
|
7669
|
+
* accept: ["image/png", "image/jpeg"],
|
|
7670
|
+
* maxSize: 1000000, // 1MB
|
|
7671
|
+
* });
|
|
7672
|
+
* ```
|
|
7673
|
+
*/
|
|
7674
|
+
function createBlobField(options = {}) {
|
|
7675
|
+
return { type: "blob", ...options };
|
|
7676
|
+
}
|
|
7677
|
+
/**
|
|
7678
|
+
* Create a datetime string field.
|
|
7679
|
+
*
|
|
7680
|
+
* This is a convenience function for creating string fields with datetime format.
|
|
7681
|
+
*
|
|
7682
|
+
* @param options - String field options
|
|
7683
|
+
* @returns A lexicon string field with datetime format
|
|
7684
|
+
*
|
|
7685
|
+
* @example
|
|
7686
|
+
* ```typescript
|
|
7687
|
+
* const createdAtField = createDatetimeField({
|
|
7688
|
+
* description: "When the record was created",
|
|
7689
|
+
* });
|
|
7690
|
+
* ```
|
|
7691
|
+
*/
|
|
7692
|
+
function createDatetimeField(options = {}) {
|
|
7693
|
+
return {
|
|
7694
|
+
type: "string",
|
|
7695
|
+
format: "datetime",
|
|
7696
|
+
...options,
|
|
7697
|
+
};
|
|
7698
|
+
}
|
|
7699
|
+
/**
|
|
7700
|
+
* Create a record definition.
|
|
7701
|
+
*
|
|
7702
|
+
* This defines the structure of records in your lexicon.
|
|
7703
|
+
*
|
|
7704
|
+
* @param properties - The record's properties (fields)
|
|
7705
|
+
* @param required - Array of required field names
|
|
7706
|
+
* @param keyType - The type of record key ("tid" for server-generated, "any" for custom)
|
|
7707
|
+
* @returns A lexicon record definition
|
|
7708
|
+
*
|
|
7709
|
+
* @example
|
|
7710
|
+
* ```typescript
|
|
7711
|
+
* const recordDef = createRecordDef(
|
|
7712
|
+
* {
|
|
7713
|
+
* $type: createStringField({ const: "org.myapp.evaluation" }),
|
|
7714
|
+
* subject: createStrongRefField({ description: "The evaluated item" }),
|
|
7715
|
+
* score: createIntegerField({ minimum: 0, maximum: 100 }),
|
|
7716
|
+
* createdAt: createDatetimeField(),
|
|
7717
|
+
* },
|
|
7718
|
+
* ["$type", "subject", "score", "createdAt"],
|
|
7719
|
+
* "tid"
|
|
7720
|
+
* );
|
|
7721
|
+
* ```
|
|
7722
|
+
*/
|
|
7723
|
+
function createRecordDef(properties, required, keyType = "tid") {
|
|
7724
|
+
return {
|
|
7725
|
+
type: "record",
|
|
7726
|
+
key: keyType,
|
|
7727
|
+
record: {
|
|
7728
|
+
type: "object",
|
|
7729
|
+
required,
|
|
7730
|
+
properties,
|
|
7731
|
+
},
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
/**
|
|
7735
|
+
* Create a complete lexicon document.
|
|
7736
|
+
*
|
|
7737
|
+
* This creates a full lexicon JSON structure that can be registered with the SDK.
|
|
7738
|
+
*
|
|
7739
|
+
* @param nsid - The NSID (Namespaced Identifier) for this lexicon
|
|
7740
|
+
* @param properties - The record's properties (fields)
|
|
7741
|
+
* @param required - Array of required field names
|
|
7742
|
+
* @param keyType - The type of record key ("tid" for server-generated, "any" for custom)
|
|
7743
|
+
* @returns A complete lexicon document
|
|
7744
|
+
*
|
|
7745
|
+
* @example
|
|
7746
|
+
* ```typescript
|
|
7747
|
+
* const lexicon = createLexiconDoc(
|
|
7748
|
+
* "org.myapp.evaluation",
|
|
7749
|
+
* {
|
|
7750
|
+
* $type: createStringField({ const: "org.myapp.evaluation" }),
|
|
7751
|
+
* subject: createStrongRefField({ description: "The evaluated item" }),
|
|
7752
|
+
* score: createIntegerField({ minimum: 0, maximum: 100 }),
|
|
7753
|
+
* methodology: createStringField({ maxLength: 500 }),
|
|
7754
|
+
* createdAt: createDatetimeField(),
|
|
7755
|
+
* },
|
|
7756
|
+
* ["$type", "subject", "score", "createdAt"],
|
|
7757
|
+
* "tid"
|
|
7758
|
+
* );
|
|
7759
|
+
*
|
|
7760
|
+
* // Register with SDK
|
|
7761
|
+
* sdk.getLexiconRegistry().registerFromJSON(lexicon);
|
|
7762
|
+
* ```
|
|
7763
|
+
*/
|
|
7764
|
+
function createLexiconDoc(nsid, properties, required, keyType = "tid") {
|
|
7765
|
+
return {
|
|
7766
|
+
lexicon: 1,
|
|
7767
|
+
id: nsid,
|
|
7768
|
+
defs: {
|
|
7769
|
+
main: createRecordDef(properties, required, keyType),
|
|
7770
|
+
},
|
|
7771
|
+
};
|
|
7772
|
+
}
|
|
7773
|
+
/**
|
|
7774
|
+
* Validate a lexicon document structure.
|
|
7775
|
+
*
|
|
7776
|
+
* Performs basic validation to ensure the lexicon follows AT Protocol conventions.
|
|
7777
|
+
* This does NOT perform full JSON schema validation.
|
|
7778
|
+
*
|
|
7779
|
+
* @param lexicon - The lexicon document to validate
|
|
7780
|
+
* @returns True if valid, false otherwise
|
|
7781
|
+
*
|
|
7782
|
+
* @example
|
|
7783
|
+
* ```typescript
|
|
7784
|
+
* const lexicon = createLexiconDoc(...);
|
|
7785
|
+
* if (validateLexiconStructure(lexicon)) {
|
|
7786
|
+
* sdk.getLexiconRegistry().registerFromJSON(lexicon);
|
|
7787
|
+
* } else {
|
|
7788
|
+
* console.error("Invalid lexicon structure");
|
|
7789
|
+
* }
|
|
7790
|
+
* ```
|
|
7791
|
+
*/
|
|
7792
|
+
function validateLexiconStructure(lexicon) {
|
|
7793
|
+
if (!lexicon || typeof lexicon !== "object") {
|
|
7794
|
+
return false;
|
|
7795
|
+
}
|
|
7796
|
+
const doc = lexicon;
|
|
7797
|
+
// Check required top-level fields
|
|
7798
|
+
if (doc.lexicon !== 1)
|
|
7799
|
+
return false;
|
|
7800
|
+
if (typeof doc.id !== "string" || !doc.id)
|
|
7801
|
+
return false;
|
|
7802
|
+
if (!doc.defs || typeof doc.defs !== "object")
|
|
7803
|
+
return false;
|
|
7804
|
+
const defs = doc.defs;
|
|
7805
|
+
if (!defs.main || typeof defs.main !== "object")
|
|
7806
|
+
return false;
|
|
7807
|
+
const main = defs.main;
|
|
7808
|
+
if (main.type !== "record")
|
|
7809
|
+
return false;
|
|
7810
|
+
if (!main.record || typeof main.record !== "object")
|
|
7811
|
+
return false;
|
|
7812
|
+
const record = main.record;
|
|
7813
|
+
if (record.type !== "object")
|
|
7814
|
+
return false;
|
|
7815
|
+
if (!Array.isArray(record.required))
|
|
7816
|
+
return false;
|
|
7817
|
+
if (!record.properties || typeof record.properties !== "object")
|
|
7818
|
+
return false;
|
|
7819
|
+
return true;
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
/**
|
|
7823
|
+
* Sidecar Pattern Utilities
|
|
7824
|
+
*
|
|
7825
|
+
* This module provides utilities for implementing the AT Protocol "sidecar pattern"
|
|
7826
|
+
* where additional records are created that reference a main record via StrongRef.
|
|
7827
|
+
*
|
|
7828
|
+
* ## The Sidecar Pattern
|
|
7829
|
+
*
|
|
7830
|
+
* In AT Protocol, the sidecar pattern uses **unidirectional references**:
|
|
7831
|
+
* - Sidecar records contain a StrongRef (uri + cid) pointing to the main record
|
|
7832
|
+
* - Main records do NOT maintain back-references to sidecars
|
|
7833
|
+
* - Sidecars are discovered by querying records that reference the main record
|
|
7834
|
+
* - Optionally, sidecars can use the same rkey as the main record (in different collections)
|
|
7835
|
+
*
|
|
7836
|
+
* ## Example Use Cases
|
|
7837
|
+
*
|
|
7838
|
+
* - Evaluations that reference hypercerts
|
|
7839
|
+
* - Comments that reference posts
|
|
7840
|
+
* - Metadata records that reference primary entities
|
|
7841
|
+
*
|
|
7842
|
+
* @see https://atproto.com/specs/record-key
|
|
7843
|
+
* @see https://atproto.com/specs/data-model
|
|
7844
|
+
*
|
|
7845
|
+
* @packageDocumentation
|
|
7846
|
+
*/
|
|
7847
|
+
/**
|
|
7848
|
+
* Create a sidecar record that references an existing record.
|
|
7849
|
+
*
|
|
7850
|
+
* This is a low-level utility that creates a single sidecar record. The sidecar
|
|
7851
|
+
* record should include a strongRef field that points to the main record.
|
|
7852
|
+
*
|
|
7853
|
+
* @param repo - The repository instance
|
|
7854
|
+
* @param collection - The collection NSID for the sidecar
|
|
7855
|
+
* @param record - The sidecar record data (should include a strongRef to the main record)
|
|
7856
|
+
* @param options - Optional creation options
|
|
7857
|
+
* @returns The created sidecar record
|
|
7858
|
+
*
|
|
7859
|
+
* @example
|
|
7860
|
+
* ```typescript
|
|
7861
|
+
* const hypercert = await repo.hypercerts.create({...});
|
|
7862
|
+
*
|
|
7863
|
+
* const evaluation = await createSidecarRecord(
|
|
7864
|
+
* repo,
|
|
7865
|
+
* "org.myapp.evaluation",
|
|
7866
|
+
* {
|
|
7867
|
+
* $type: "org.myapp.evaluation",
|
|
7868
|
+
* subject: { uri: hypercert.hypercertUri, cid: hypercert.hypercertCid },
|
|
7869
|
+
* score: 85,
|
|
7870
|
+
* createdAt: new Date().toISOString(),
|
|
7871
|
+
* }
|
|
7872
|
+
* );
|
|
7873
|
+
* ```
|
|
7874
|
+
*/
|
|
7875
|
+
async function createSidecarRecord(repo, collection, record, options = {}) {
|
|
7876
|
+
return await repo.records.create({
|
|
7877
|
+
collection,
|
|
7878
|
+
record,
|
|
7879
|
+
rkey: options.rkey,
|
|
7880
|
+
});
|
|
7881
|
+
}
|
|
7882
|
+
/**
|
|
7883
|
+
* Attach a sidecar record to an existing main record.
|
|
7884
|
+
*
|
|
7885
|
+
* This creates a new record that references an existing record via strongRef.
|
|
7886
|
+
* It's a higher-level convenience function that wraps `createSidecarRecord`.
|
|
7887
|
+
*
|
|
7888
|
+
* @param repo - The repository instance
|
|
7889
|
+
* @param params - Parameters including the main record reference and sidecar definition
|
|
7890
|
+
* @returns Both the main record reference and the created sidecar
|
|
7891
|
+
*
|
|
7892
|
+
* @example
|
|
7893
|
+
* ```typescript
|
|
7894
|
+
* const hypercert = await repo.hypercerts.create({...});
|
|
7895
|
+
*
|
|
7896
|
+
* const result = await attachSidecar(repo, {
|
|
7897
|
+
* mainRecord: {
|
|
7898
|
+
* uri: hypercert.hypercertUri,
|
|
7899
|
+
* cid: hypercert.hypercertCid,
|
|
7900
|
+
* },
|
|
7901
|
+
* sidecar: {
|
|
7902
|
+
* collection: "org.myapp.evaluation",
|
|
7903
|
+
* record: {
|
|
7904
|
+
* $type: "org.myapp.evaluation",
|
|
7905
|
+
* subject: { uri: hypercert.hypercertUri, cid: hypercert.hypercertCid },
|
|
7906
|
+
* score: 85,
|
|
7907
|
+
* createdAt: new Date().toISOString(),
|
|
7908
|
+
* },
|
|
7909
|
+
* },
|
|
7910
|
+
* });
|
|
7911
|
+
* ```
|
|
7912
|
+
*/
|
|
7913
|
+
async function attachSidecar(repo, params) {
|
|
7914
|
+
const sidecarRecord = await createSidecarRecord(repo, params.sidecar.collection, params.sidecar.record, {
|
|
7915
|
+
rkey: params.sidecar.rkey,
|
|
7916
|
+
});
|
|
7917
|
+
return {
|
|
7918
|
+
mainRecord: params.mainRecord,
|
|
7919
|
+
sidecarRecord,
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
/**
|
|
7923
|
+
* Create a main record and multiple sidecar records in sequence.
|
|
7924
|
+
*
|
|
7925
|
+
* This orchestrates the creation of a main record followed by one or more
|
|
7926
|
+
* sidecar records that reference it. This is useful for workflows like:
|
|
7927
|
+
* - Creating a project with multiple hypercert claims
|
|
7928
|
+
* - Creating a hypercert with evidence and evaluation records
|
|
7929
|
+
*
|
|
7930
|
+
* @param repo - The repository instance
|
|
7931
|
+
* @param params - Parameters including the main record and sidecar definitions
|
|
7932
|
+
* @returns The main record and all created sidecar records
|
|
7933
|
+
*
|
|
7934
|
+
* @example
|
|
7935
|
+
* ```typescript
|
|
7936
|
+
* const result = await createWithSidecars(repo, {
|
|
7937
|
+
* main: {
|
|
7938
|
+
* collection: "org.hypercerts.project",
|
|
7939
|
+
* record: {
|
|
7940
|
+
* $type: "org.hypercerts.project",
|
|
7941
|
+
* title: "Climate Initiative 2024",
|
|
7942
|
+
* description: "Our climate work",
|
|
7943
|
+
* createdAt: new Date().toISOString(),
|
|
7944
|
+
* },
|
|
7945
|
+
* },
|
|
7946
|
+
* sidecars: [
|
|
7947
|
+
* {
|
|
7948
|
+
* collection: "org.hypercerts.claim.activity",
|
|
7949
|
+
* record: {
|
|
7950
|
+
* $type: "org.hypercerts.claim.activity",
|
|
7951
|
+
* title: "Tree Planting",
|
|
7952
|
+
* // ... other hypercert fields
|
|
7953
|
+
* // Note: If you need to reference the main record, you must wait
|
|
7954
|
+
* // for result.main and then call batchCreateSidecars separately
|
|
7955
|
+
* },
|
|
7956
|
+
* },
|
|
7957
|
+
* {
|
|
7958
|
+
* collection: "org.hypercerts.claim.activity",
|
|
7959
|
+
* record: {
|
|
7960
|
+
* $type: "org.hypercerts.claim.activity",
|
|
7961
|
+
* title: "Carbon Measurement",
|
|
7962
|
+
* // ... other hypercert fields
|
|
7963
|
+
* },
|
|
7964
|
+
* },
|
|
7965
|
+
* ],
|
|
7966
|
+
* });
|
|
7967
|
+
*
|
|
7968
|
+
* console.log(result.main.uri); // Main project record
|
|
7969
|
+
* console.log(result.sidecars.length); // 2 hypercert sidecars
|
|
7970
|
+
* ```
|
|
7971
|
+
*/
|
|
7972
|
+
async function createWithSidecars(repo, params) {
|
|
7973
|
+
// Create the main record first
|
|
7974
|
+
const main = await repo.records.create({
|
|
7975
|
+
collection: params.main.collection,
|
|
7976
|
+
record: params.main.record,
|
|
7977
|
+
rkey: params.main.rkey,
|
|
7978
|
+
});
|
|
7979
|
+
// Create all sidecar records
|
|
7980
|
+
const sidecars = [];
|
|
7981
|
+
for (const sidecar of params.sidecars) {
|
|
7982
|
+
const created = await repo.records.create({
|
|
7983
|
+
collection: sidecar.collection,
|
|
7984
|
+
record: sidecar.record,
|
|
7985
|
+
rkey: sidecar.rkey,
|
|
7986
|
+
});
|
|
7987
|
+
sidecars.push(created);
|
|
7988
|
+
}
|
|
7989
|
+
return { main, sidecars };
|
|
7990
|
+
}
|
|
7991
|
+
/**
|
|
7992
|
+
* Batch create multiple sidecar records.
|
|
7993
|
+
*
|
|
7994
|
+
* This is useful when you want to add multiple related records
|
|
7995
|
+
* efficiently. The sidecar records should already contain any necessary
|
|
7996
|
+
* references to the main record in their data.
|
|
7997
|
+
*
|
|
7998
|
+
* @param repo - The repository instance
|
|
7999
|
+
* @param sidecars - Array of sidecar definitions (records should include references)
|
|
8000
|
+
* @returns Array of created sidecar records
|
|
8001
|
+
*
|
|
8002
|
+
* @example
|
|
8003
|
+
* ```typescript
|
|
8004
|
+
* const hypercert = await repo.hypercerts.create({...});
|
|
8005
|
+
* const mainRef = { uri: hypercert.hypercertUri, cid: hypercert.hypercertCid };
|
|
8006
|
+
*
|
|
8007
|
+
* const evaluations = await batchCreateSidecars(repo, [
|
|
8008
|
+
* {
|
|
8009
|
+
* collection: "org.myapp.evaluation",
|
|
8010
|
+
* record: {
|
|
8011
|
+
* $type: "org.myapp.evaluation",
|
|
8012
|
+
* subject: mainRef, // Reference already included
|
|
8013
|
+
* score: 85,
|
|
8014
|
+
* methodology: "Peer review",
|
|
8015
|
+
* createdAt: new Date().toISOString(),
|
|
8016
|
+
* },
|
|
8017
|
+
* },
|
|
8018
|
+
* {
|
|
8019
|
+
* collection: "org.myapp.comment",
|
|
8020
|
+
* record: {
|
|
8021
|
+
* $type: "org.myapp.comment",
|
|
8022
|
+
* subject: mainRef, // Reference already included
|
|
8023
|
+
* text: "Great work!",
|
|
8024
|
+
* createdAt: new Date().toISOString(),
|
|
8025
|
+
* },
|
|
8026
|
+
* },
|
|
8027
|
+
* ]);
|
|
8028
|
+
* ```
|
|
8029
|
+
*/
|
|
8030
|
+
async function batchCreateSidecars(repo, sidecars) {
|
|
8031
|
+
const results = [];
|
|
8032
|
+
for (const sidecar of sidecars) {
|
|
8033
|
+
const result = await createSidecarRecord(repo, sidecar.collection, sidecar.record, {
|
|
8034
|
+
rkey: sidecar.rkey,
|
|
8035
|
+
});
|
|
8036
|
+
results.push(result);
|
|
8037
|
+
}
|
|
8038
|
+
return results;
|
|
5667
8039
|
}
|
|
5668
8040
|
|
|
5669
8041
|
/**
|
|
@@ -5825,9 +8197,13 @@ Object.defineProperty(exports, "OrgHypercertsClaimCollection", {
|
|
|
5825
8197
|
enumerable: true,
|
|
5826
8198
|
get: function () { return lexicon.OrgHypercertsClaimCollection; }
|
|
5827
8199
|
});
|
|
5828
|
-
Object.defineProperty(exports, "
|
|
8200
|
+
Object.defineProperty(exports, "OrgHypercertsClaimContributionDetails", {
|
|
8201
|
+
enumerable: true,
|
|
8202
|
+
get: function () { return lexicon.OrgHypercertsClaimContributionDetails; }
|
|
8203
|
+
});
|
|
8204
|
+
Object.defineProperty(exports, "OrgHypercertsClaimContributorInformation", {
|
|
5829
8205
|
enumerable: true,
|
|
5830
|
-
get: function () { return lexicon.
|
|
8206
|
+
get: function () { return lexicon.OrgHypercertsClaimContributorInformation; }
|
|
5831
8207
|
});
|
|
5832
8208
|
Object.defineProperty(exports, "OrgHypercertsClaimEvaluation", {
|
|
5833
8209
|
enumerable: true,
|
|
@@ -5841,10 +8217,6 @@ Object.defineProperty(exports, "OrgHypercertsClaimMeasurement", {
|
|
|
5841
8217
|
enumerable: true,
|
|
5842
8218
|
get: function () { return lexicon.OrgHypercertsClaimMeasurement; }
|
|
5843
8219
|
});
|
|
5844
|
-
Object.defineProperty(exports, "OrgHypercertsClaimProject", {
|
|
5845
|
-
enumerable: true,
|
|
5846
|
-
get: function () { return lexicon.OrgHypercertsClaimProject; }
|
|
5847
|
-
});
|
|
5848
8220
|
Object.defineProperty(exports, "OrgHypercertsClaimRights", {
|
|
5849
8221
|
enumerable: true,
|
|
5850
8222
|
get: function () { return lexicon.OrgHypercertsClaimRights; }
|
|
@@ -5853,6 +8225,10 @@ Object.defineProperty(exports, "OrgHypercertsFundingReceipt", {
|
|
|
5853
8225
|
enumerable: true,
|
|
5854
8226
|
get: function () { return lexicon.OrgHypercertsFundingReceipt; }
|
|
5855
8227
|
});
|
|
8228
|
+
Object.defineProperty(exports, "OrgHypercertsHelperWorkScopeTag", {
|
|
8229
|
+
enumerable: true,
|
|
8230
|
+
get: function () { return lexicon.OrgHypercertsHelperWorkScopeTag; }
|
|
8231
|
+
});
|
|
5856
8232
|
Object.defineProperty(exports, "validate", {
|
|
5857
8233
|
enumerable: true,
|
|
5858
8234
|
get: function () { return lexicon.validate; }
|
|
@@ -5865,6 +8241,7 @@ exports.AccountActionSchema = AccountActionSchema;
|
|
|
5865
8241
|
exports.AccountAttrSchema = AccountAttrSchema;
|
|
5866
8242
|
exports.AccountPermissionSchema = AccountPermissionSchema;
|
|
5867
8243
|
exports.AuthenticationError = AuthenticationError;
|
|
8244
|
+
exports.BaseOperations = BaseOperations;
|
|
5868
8245
|
exports.BlobPermissionSchema = BlobPermissionSchema;
|
|
5869
8246
|
exports.CollaboratorPermissionsSchema = CollaboratorPermissionsSchema;
|
|
5870
8247
|
exports.CollaboratorSchema = CollaboratorSchema;
|
|
@@ -5876,6 +8253,7 @@ exports.IdentityPermissionSchema = IdentityPermissionSchema;
|
|
|
5876
8253
|
exports.InMemorySessionStore = InMemorySessionStore;
|
|
5877
8254
|
exports.InMemoryStateStore = InMemoryStateStore;
|
|
5878
8255
|
exports.IncludePermissionSchema = IncludePermissionSchema;
|
|
8256
|
+
exports.LexiconRegistry = LexiconRegistry;
|
|
5879
8257
|
exports.MimeTypeSchema = MimeTypeSchema;
|
|
5880
8258
|
exports.NetworkError = NetworkError;
|
|
5881
8259
|
exports.NsidSchema = NsidSchema;
|
|
@@ -5895,13 +8273,37 @@ exports.TRANSITION_SCOPES = TRANSITION_SCOPES;
|
|
|
5895
8273
|
exports.TimeoutConfigSchema = TimeoutConfigSchema;
|
|
5896
8274
|
exports.TransitionScopeSchema = TransitionScopeSchema;
|
|
5897
8275
|
exports.ValidationError = ValidationError;
|
|
8276
|
+
exports.attachSidecar = attachSidecar;
|
|
8277
|
+
exports.batchCreateSidecars = batchCreateSidecars;
|
|
8278
|
+
exports.buildAtUri = buildAtUri;
|
|
5898
8279
|
exports.buildScope = buildScope;
|
|
5899
8280
|
exports.createATProtoSDK = createATProtoSDK;
|
|
8281
|
+
exports.createArrayField = createArrayField;
|
|
8282
|
+
exports.createBlobField = createBlobField;
|
|
8283
|
+
exports.createBooleanField = createBooleanField;
|
|
8284
|
+
exports.createDatetimeField = createDatetimeField;
|
|
8285
|
+
exports.createIntegerField = createIntegerField;
|
|
8286
|
+
exports.createLexiconDoc = createLexiconDoc;
|
|
8287
|
+
exports.createNumberField = createNumberField;
|
|
8288
|
+
exports.createObjectField = createObjectField;
|
|
8289
|
+
exports.createRecordDef = createRecordDef;
|
|
8290
|
+
exports.createSidecarRecord = createSidecarRecord;
|
|
8291
|
+
exports.createStringField = createStringField;
|
|
8292
|
+
exports.createStrongRef = createStrongRef;
|
|
8293
|
+
exports.createStrongRefField = createStrongRefField;
|
|
8294
|
+
exports.createStrongRefFromResult = createStrongRefFromResult;
|
|
8295
|
+
exports.createWithSidecars = createWithSidecars;
|
|
8296
|
+
exports.extractRkeyFromUri = extractRkeyFromUri;
|
|
5900
8297
|
exports.hasAllPermissions = hasAllPermissions;
|
|
5901
8298
|
exports.hasAnyPermission = hasAnyPermission;
|
|
5902
8299
|
exports.hasPermission = hasPermission;
|
|
8300
|
+
exports.isStrongRef = isStrongRef;
|
|
8301
|
+
exports.isValidAtUri = isValidAtUri;
|
|
5903
8302
|
exports.mergeScopes = mergeScopes;
|
|
8303
|
+
exports.parseAtUri = parseAtUri;
|
|
5904
8304
|
exports.parseScope = parseScope;
|
|
5905
8305
|
exports.removePermissions = removePermissions;
|
|
8306
|
+
exports.validateLexiconStructure = validateLexiconStructure;
|
|
5906
8307
|
exports.validateScope = validateScope;
|
|
8308
|
+
exports.validateStrongRef = validateStrongRef;
|
|
5907
8309
|
//# sourceMappingURL=index.cjs.map
|