@hypercerts-org/sdk-core 0.10.0-beta.8 → 0.10.0-beta.9
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 +332 -0
- package/README.md +168 -78
- package/dist/index.cjs +1692 -1282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +546 -179
- package/dist/index.mjs +1691 -1286
- package/dist/index.mjs.map +1 -1
- package/dist/lexicons.cjs +9 -0
- package/dist/lexicons.cjs.map +1 -1
- package/dist/lexicons.d.ts +8 -0
- package/dist/lexicons.mjs +10 -1
- package/dist/lexicons.mjs.map +1 -1
- package/dist/testing.cjs +11 -5
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.ts +25 -28
- package/dist/testing.mjs +11 -5
- package/dist/testing.mjs.map +1 -1
- package/dist/types.cjs +15 -16
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +332 -143
- package/dist/types.mjs +15 -16
- package/dist/types.mjs.map +1 -1
- package/package.json +8 -4
package/dist/index.cjs
CHANGED
|
@@ -4,9 +4,52 @@ var oauthClientNode = require('@atproto/oauth-client-node');
|
|
|
4
4
|
var zod = require('zod');
|
|
5
5
|
var api = require('@atproto/api');
|
|
6
6
|
var lexicon$1 = require('@atproto/lexicon');
|
|
7
|
+
var cid = require('multiformats/cid');
|
|
7
8
|
var lexicon = require('@hypercerts-org/lexicon');
|
|
8
9
|
var eventemitter3 = require('eventemitter3');
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Regular expression to match a valid URI with a scheme.
|
|
13
|
+
*
|
|
14
|
+
* Matches strings that start with a scheme (one or more alphanumeric characters,
|
|
15
|
+
* plus, period, or hyphen) followed by a colon. This covers schemes that the
|
|
16
|
+
* native URL constructor may not support (e.g., `at://`, `ipfs://`).
|
|
17
|
+
*
|
|
18
|
+
* @see https://www.rfc-editor.org/rfc/rfc3986#section-3.1
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const URI_SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:/;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a string is a valid URI with a scheme.
|
|
24
|
+
*
|
|
25
|
+
* Validates that the string is a properly formatted URI. Uses the native
|
|
26
|
+
* `URL` constructor for standard schemes (http, https, ftp, etc.) and falls
|
|
27
|
+
* back to scheme detection for non-standard schemes like `at://` and `ipfs://`
|
|
28
|
+
* that the `URL` constructor does not support.
|
|
29
|
+
*
|
|
30
|
+
* @param uri - The string to validate
|
|
31
|
+
* @returns True if the string is a valid URI with a scheme, false otherwise
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* isValidUri("https://example.com/report.pdf"); // true
|
|
36
|
+
* isValidUri("ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); // true
|
|
37
|
+
* isValidUri("at://did:plc:abc/org.col/rkey"); // true
|
|
38
|
+
* isValidUri("not-a-uri"); // false
|
|
39
|
+
* isValidUri(""); // false
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
function isValidUri(uri) {
|
|
43
|
+
if (!uri)
|
|
44
|
+
return false;
|
|
45
|
+
try {
|
|
46
|
+
new URL(uri);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return URI_SCHEME_REGEX.test(uri);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
10
53
|
/**
|
|
11
54
|
* Type guard to check if a URL is a loopback address.
|
|
12
55
|
*
|
|
@@ -1642,7 +1685,7 @@ function validateScope(scope) {
|
|
|
1642
1685
|
const permissions = parseScope(scope);
|
|
1643
1686
|
const invalidPermissions = [];
|
|
1644
1687
|
// Pattern for valid permission prefixes
|
|
1645
|
-
const validPrefixes = /^(atproto
|
|
1688
|
+
const validPrefixes = /^(atproto$|transition:|account:|repo[:?]|blob[:?]|rpc[:?]|identity:|include:)/;
|
|
1646
1689
|
for (const permission of permissions) {
|
|
1647
1690
|
if (!validPrefixes.test(permission)) {
|
|
1648
1691
|
invalidPermissions.push(permission);
|
|
@@ -1686,7 +1729,7 @@ function validateScope(scope) {
|
|
|
1686
1729
|
* jwksUri: "https://my-app.com/.well-known/jwks.json",
|
|
1687
1730
|
* jwkPrivate: process.env.JWK_PRIVATE_KEY!,
|
|
1688
1731
|
* },
|
|
1689
|
-
*
|
|
1732
|
+
* handleResolver: "https://pds-eu-west4.test.certified.app",
|
|
1690
1733
|
* });
|
|
1691
1734
|
*
|
|
1692
1735
|
* // Start authorization
|
|
@@ -1756,7 +1799,7 @@ class OAuthClient {
|
|
|
1756
1799
|
keyset,
|
|
1757
1800
|
stateStore: this.createStateStoreAdapter(stateStore),
|
|
1758
1801
|
sessionStore: this.createSessionStoreAdapter(sessionStore),
|
|
1759
|
-
handleResolver: this.config.
|
|
1802
|
+
handleResolver: this.config.handleResolver,
|
|
1760
1803
|
fetch: this.config.fetch ?? fetchWithTimeout,
|
|
1761
1804
|
});
|
|
1762
1805
|
return this.client;
|
|
@@ -2878,6 +2921,148 @@ class RecordOperationsImpl {
|
|
|
2878
2921
|
}
|
|
2879
2922
|
}
|
|
2880
2923
|
|
|
2924
|
+
/**
|
|
2925
|
+
* Validates that a string is a valid DID format.
|
|
2926
|
+
*
|
|
2927
|
+
* DIDs must follow the format: `did:<method>:<method-specific-id>`
|
|
2928
|
+
* where method is lowercase letters and digits, and the identifier contains
|
|
2929
|
+
* alphanumeric characters plus `.`, `_`, `:`, `%`, and `-`.
|
|
2930
|
+
*
|
|
2931
|
+
* @param did - The string to validate
|
|
2932
|
+
* @returns true if the string is a valid DID format
|
|
2933
|
+
*
|
|
2934
|
+
* @example
|
|
2935
|
+
* ```typescript
|
|
2936
|
+
* isValidDid("did:plc:ewvi7nxzyoun6zhxrhs64oiz"); // true
|
|
2937
|
+
* isValidDid("did:web:example.com"); // true
|
|
2938
|
+
* isValidDid("not-a-did"); // false
|
|
2939
|
+
* isValidDid("did:"); // false
|
|
2940
|
+
* ```
|
|
2941
|
+
*
|
|
2942
|
+
* @see https://www.w3.org/TR/did-core/#did-syntax for DID syntax specification
|
|
2943
|
+
*/
|
|
2944
|
+
function isValidDid(did) {
|
|
2945
|
+
// DID format: did:<method>:<method-specific-id>
|
|
2946
|
+
// Method: lowercase letters and digits (per W3C DID Core spec)
|
|
2947
|
+
// Identifier: alphanumeric plus . _ : % -
|
|
2948
|
+
// method-specific-id must end with at least one non-colon idchar (W3C DID Core 1.0)
|
|
2949
|
+
return /^did:[a-z0-9]+:(?:[a-zA-Z0-9._%-]+:)*[a-zA-Z0-9._%-]+$/.test(did);
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Zod schema for collaborator permissions in SDS repositories.
|
|
2953
|
+
*
|
|
2954
|
+
* Defines the granular permissions a collaborator can have on a shared repository.
|
|
2955
|
+
* Permissions follow a hierarchical model where higher-level permissions
|
|
2956
|
+
* typically imply lower-level ones.
|
|
2957
|
+
*/
|
|
2958
|
+
const CollaboratorPermissionsSchema = zod.z.object({
|
|
2959
|
+
/**
|
|
2960
|
+
* Can read/view records in the repository.
|
|
2961
|
+
* This is the most basic permission level.
|
|
2962
|
+
*/
|
|
2963
|
+
read: zod.z.boolean(),
|
|
2964
|
+
/**
|
|
2965
|
+
* Can create new records in the repository.
|
|
2966
|
+
* Typically implies `read` permission.
|
|
2967
|
+
*/
|
|
2968
|
+
create: zod.z.boolean(),
|
|
2969
|
+
/**
|
|
2970
|
+
* Can modify existing records in the repository.
|
|
2971
|
+
* Typically implies `read` and `create` permissions.
|
|
2972
|
+
*/
|
|
2973
|
+
update: zod.z.boolean(),
|
|
2974
|
+
/**
|
|
2975
|
+
* Can delete records from the repository.
|
|
2976
|
+
* Typically implies `read`, `create`, and `update` permissions.
|
|
2977
|
+
*/
|
|
2978
|
+
delete: zod.z.boolean(),
|
|
2979
|
+
/**
|
|
2980
|
+
* Can manage collaborators and their permissions.
|
|
2981
|
+
* Administrative permission that allows inviting/removing collaborators.
|
|
2982
|
+
*/
|
|
2983
|
+
admin: zod.z.boolean(),
|
|
2984
|
+
/**
|
|
2985
|
+
* Full ownership of the repository.
|
|
2986
|
+
* Owners have all permissions and cannot be removed by other admins.
|
|
2987
|
+
* There must always be at least one owner.
|
|
2988
|
+
*/
|
|
2989
|
+
owner: zod.z.boolean(),
|
|
2990
|
+
});
|
|
2991
|
+
/**
|
|
2992
|
+
* Zod schema for SDS organization data.
|
|
2993
|
+
*
|
|
2994
|
+
* Organizations are top-level entities in SDS that can own repositories
|
|
2995
|
+
* and have multiple collaborators with different permission levels.
|
|
2996
|
+
*/
|
|
2997
|
+
const OrganizationSchema = zod.z.object({
|
|
2998
|
+
/**
|
|
2999
|
+
* The organization's DID - unique identifier.
|
|
3000
|
+
* Format: "did:plc:..." or "did:web:..."
|
|
3001
|
+
*/
|
|
3002
|
+
did: zod.z.string(),
|
|
3003
|
+
/**
|
|
3004
|
+
* The organization's handle - human-readable identifier.
|
|
3005
|
+
* Format: "orgname.sds.hypercerts.org" or similar
|
|
3006
|
+
*/
|
|
3007
|
+
handle: zod.z.string(),
|
|
3008
|
+
/**
|
|
3009
|
+
* Display name for the organization.
|
|
3010
|
+
*/
|
|
3011
|
+
name: zod.z.string(),
|
|
3012
|
+
/**
|
|
3013
|
+
* Optional description of the organization's purpose.
|
|
3014
|
+
*/
|
|
3015
|
+
description: zod.z.string().optional(),
|
|
3016
|
+
/**
|
|
3017
|
+
* ISO 8601 timestamp when the organization was created.
|
|
3018
|
+
* Format: "2024-01-15T10:30:00.000Z"
|
|
3019
|
+
*/
|
|
3020
|
+
createdAt: zod.z.string(),
|
|
3021
|
+
/**
|
|
3022
|
+
* The current user's permissions within this organization.
|
|
3023
|
+
*/
|
|
3024
|
+
permissions: CollaboratorPermissionsSchema,
|
|
3025
|
+
/**
|
|
3026
|
+
* How the current user relates to this organization.
|
|
3027
|
+
* - `"owner"`: User created or owns the organization
|
|
3028
|
+
* - `"shared"`: User was invited to collaborate (has permissions)
|
|
3029
|
+
* - `"none"`: User has no access to this organization
|
|
3030
|
+
*/
|
|
3031
|
+
accessType: zod.z.enum(["owner", "shared", "none"]),
|
|
3032
|
+
});
|
|
3033
|
+
/**
|
|
3034
|
+
* Zod schema for collaborator data.
|
|
3035
|
+
*
|
|
3036
|
+
* Represents a user who has been granted access to a shared repository
|
|
3037
|
+
* or organization with specific permissions.
|
|
3038
|
+
*/
|
|
3039
|
+
const CollaboratorSchema = zod.z.object({
|
|
3040
|
+
/**
|
|
3041
|
+
* The collaborator's DID - their unique identifier.
|
|
3042
|
+
* Format: "did:plc:..." or "did:web:..."
|
|
3043
|
+
*/
|
|
3044
|
+
userDid: zod.z.string(),
|
|
3045
|
+
/**
|
|
3046
|
+
* The permissions granted to this collaborator.
|
|
3047
|
+
*/
|
|
3048
|
+
permissions: CollaboratorPermissionsSchema,
|
|
3049
|
+
/**
|
|
3050
|
+
* DID of the user who granted these permissions.
|
|
3051
|
+
* Useful for audit trails.
|
|
3052
|
+
*/
|
|
3053
|
+
grantedBy: zod.z.string(),
|
|
3054
|
+
/**
|
|
3055
|
+
* ISO 8601 timestamp when permissions were granted.
|
|
3056
|
+
* Format: "2024-01-15T10:30:00.000Z"
|
|
3057
|
+
*/
|
|
3058
|
+
grantedAt: zod.z.string(),
|
|
3059
|
+
/**
|
|
3060
|
+
* ISO 8601 timestamp when permissions were revoked, if applicable.
|
|
3061
|
+
* Undefined if the collaborator is still active.
|
|
3062
|
+
*/
|
|
3063
|
+
revokedAt: zod.z.string().optional(),
|
|
3064
|
+
});
|
|
3065
|
+
|
|
2881
3066
|
/**
|
|
2882
3067
|
* BlobOperationsImpl - Blob upload and retrieval operations.
|
|
2883
3068
|
*
|
|
@@ -2941,6 +3126,9 @@ class BlobOperationsImpl {
|
|
|
2941
3126
|
this.repoDid = repoDid;
|
|
2942
3127
|
this._serverUrl = _serverUrl;
|
|
2943
3128
|
this.isSDS = isSDS;
|
|
3129
|
+
if (!isValidDid(repoDid)) {
|
|
3130
|
+
throw new ValidationError(`Invalid DID format: "${repoDid}". DIDs must start with "did:" (e.g., "did:plc:abc123")`);
|
|
3131
|
+
}
|
|
2944
3132
|
}
|
|
2945
3133
|
/**
|
|
2946
3134
|
* Uploads a blob to the server.
|
|
@@ -3000,11 +3188,7 @@ class BlobOperationsImpl {
|
|
|
3000
3188
|
if (!result.success) {
|
|
3001
3189
|
throw new NetworkError("Failed to upload blob");
|
|
3002
3190
|
}
|
|
3003
|
-
return
|
|
3004
|
-
ref: { $link: result.data.blob.ref.toString() },
|
|
3005
|
-
mimeType: result.data.blob.mimeType,
|
|
3006
|
-
size: result.data.blob.size,
|
|
3007
|
-
};
|
|
3191
|
+
return result.data.blob;
|
|
3008
3192
|
}
|
|
3009
3193
|
catch (error) {
|
|
3010
3194
|
if (error instanceof NetworkError)
|
|
@@ -3038,13 +3222,18 @@ class BlobOperationsImpl {
|
|
|
3038
3222
|
if (!response.ok) {
|
|
3039
3223
|
throw new NetworkError(`SDS blob upload failed: ${response.statusText}`);
|
|
3040
3224
|
}
|
|
3225
|
+
// SDS returns { blob: { ref: { $link: string }, mimeType: string, size: number } }
|
|
3226
|
+
// which is a JSON-serialized blob ref, not a BlobRef instance.
|
|
3227
|
+
// Construct a BlobRef directly using CID.parse to preserve the size from the SDS response.
|
|
3041
3228
|
const result = (await response.json());
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3229
|
+
let cid$1;
|
|
3230
|
+
try {
|
|
3231
|
+
cid$1 = cid.CID.parse(result.blob.ref.$link);
|
|
3232
|
+
}
|
|
3233
|
+
catch {
|
|
3234
|
+
throw new NetworkError("SDS blob upload returned an invalid blob reference");
|
|
3235
|
+
}
|
|
3236
|
+
return new lexicon$1.BlobRef(cid$1, result.blob.mimeType, result.blob.size);
|
|
3048
3237
|
}
|
|
3049
3238
|
/**
|
|
3050
3239
|
* Retrieves a blob by its CID.
|
|
@@ -3110,556 +3299,1125 @@ class BlobOperationsImpl {
|
|
|
3110
3299
|
}
|
|
3111
3300
|
|
|
3112
3301
|
/**
|
|
3113
|
-
*
|
|
3114
|
-
* @packageDocumentation
|
|
3115
|
-
*/
|
|
3116
|
-
/**
|
|
3117
|
-
* Converts a blob upload result to JsonBlobRef format.
|
|
3302
|
+
* Lexicons entrypoint - Lexicon definitions and registry.
|
|
3118
3303
|
*
|
|
3119
|
-
*
|
|
3120
|
-
*
|
|
3304
|
+
* This sub-entrypoint exports the lexicon registry and hypercert
|
|
3305
|
+
* lexicon constants for working with AT Protocol record schemas.
|
|
3121
3306
|
*
|
|
3122
|
-
* @
|
|
3123
|
-
*
|
|
3124
|
-
*/
|
|
3125
|
-
function uploadResultToBlobRef(uploadResult) {
|
|
3126
|
-
return {
|
|
3127
|
-
$type: "blob",
|
|
3128
|
-
ref: uploadResult.ref,
|
|
3129
|
-
mimeType: uploadResult.mimeType,
|
|
3130
|
-
size: uploadResult.size,
|
|
3131
|
-
};
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
/**
|
|
3135
|
-
* ProfileOperationsImpl - User profile operations.
|
|
3307
|
+
* @remarks
|
|
3308
|
+
* Import from `@hypercerts-org/sdk/lexicons`:
|
|
3136
3309
|
*
|
|
3137
|
-
*
|
|
3138
|
-
*
|
|
3310
|
+
* ```typescript
|
|
3311
|
+
* import {
|
|
3312
|
+
* LexiconRegistry,
|
|
3313
|
+
* HYPERCERT_LEXICONS,
|
|
3314
|
+
* HYPERCERT_COLLECTIONS,
|
|
3315
|
+
* } from "@hypercerts-org/sdk/lexicons";
|
|
3316
|
+
* ```
|
|
3139
3317
|
*
|
|
3140
|
-
*
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
*
|
|
3318
|
+
* **Exports**:
|
|
3319
|
+
* - {@link LexiconRegistry} - Registry for managing and validating lexicons
|
|
3320
|
+
* - {@link HYPERCERT_LEXICONS} - Array of all hypercert lexicon documents
|
|
3321
|
+
* - {@link HYPERCERT_COLLECTIONS} - Constants for collection NSIDs
|
|
3144
3322
|
*
|
|
3145
|
-
*
|
|
3146
|
-
*
|
|
3147
|
-
*
|
|
3323
|
+
* @example Using collection constants
|
|
3324
|
+
* ```typescript
|
|
3325
|
+
* import { HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk/lexicons";
|
|
3148
3326
|
*
|
|
3149
|
-
*
|
|
3150
|
-
*
|
|
3151
|
-
*
|
|
3327
|
+
* // List hypercerts using the correct collection name
|
|
3328
|
+
* const records = await repo.records.list({
|
|
3329
|
+
* collection: HYPERCERT_COLLECTIONS.RECORD,
|
|
3330
|
+
* });
|
|
3152
3331
|
*
|
|
3153
|
-
*
|
|
3154
|
-
*
|
|
3155
|
-
*
|
|
3156
|
-
*
|
|
3157
|
-
*
|
|
3158
|
-
* - `banner`: Banner image blob reference
|
|
3159
|
-
* - `website`: User's website URL (may not be available on all servers)
|
|
3332
|
+
* // List contributions
|
|
3333
|
+
* const contributions = await repo.records.list({
|
|
3334
|
+
* collection: HYPERCERT_COLLECTIONS.CONTRIBUTION,
|
|
3335
|
+
* });
|
|
3336
|
+
* ```
|
|
3160
3337
|
*
|
|
3161
|
-
* @example
|
|
3338
|
+
* @example Custom lexicon registration
|
|
3162
3339
|
* ```typescript
|
|
3163
|
-
*
|
|
3164
|
-
*
|
|
3165
|
-
*
|
|
3166
|
-
*
|
|
3167
|
-
* // Update profile
|
|
3168
|
-
* await repo.profile.update({
|
|
3169
|
-
* displayName: "New Name",
|
|
3170
|
-
* description: "Updated bio",
|
|
3171
|
-
* });
|
|
3340
|
+
* import { LexiconRegistry } from "@hypercerts-org/sdk/lexicons";
|
|
3341
|
+
*
|
|
3342
|
+
* const registry = sdk.getLexiconRegistry();
|
|
3172
3343
|
*
|
|
3173
|
-
* //
|
|
3174
|
-
*
|
|
3175
|
-
*
|
|
3344
|
+
* // Register custom lexicon
|
|
3345
|
+
* registry.register({
|
|
3346
|
+
* lexicon: 1,
|
|
3347
|
+
* id: "org.myapp.customRecord",
|
|
3348
|
+
* defs: { ... },
|
|
3349
|
+
* });
|
|
3176
3350
|
*
|
|
3177
|
-
* //
|
|
3178
|
-
*
|
|
3351
|
+
* // Validate a record
|
|
3352
|
+
* const result = registry.validate("org.myapp.customRecord", record);
|
|
3353
|
+
* if (!result.valid) {
|
|
3354
|
+
* console.error(result.error);
|
|
3355
|
+
* }
|
|
3179
3356
|
* ```
|
|
3180
3357
|
*
|
|
3181
|
-
* @
|
|
3358
|
+
* @packageDocumentation
|
|
3182
3359
|
*/
|
|
3183
|
-
|
|
3360
|
+
/**
|
|
3361
|
+
* All hypercert-related lexicons for registration with AT Protocol Agent.
|
|
3362
|
+
* This array contains all lexicon documents from the published package.
|
|
3363
|
+
*/
|
|
3364
|
+
const HYPERCERT_LEXICONS = [
|
|
3365
|
+
lexicon.CERTIFIED_DEFS_LEXICON_JSON,
|
|
3366
|
+
lexicon.LOCATION_LEXICON_JSON,
|
|
3367
|
+
lexicon.STRONG_REF_LEXICON_JSON,
|
|
3368
|
+
lexicon.HYPERCERTS_DEFS_LEXICON_JSON,
|
|
3369
|
+
lexicon.ACTIVITY_LEXICON_JSON,
|
|
3370
|
+
lexicon.COLLECTION_LEXICON_JSON,
|
|
3371
|
+
lexicon.CONTRIBUTION_DETAILS_LEXICON_JSON,
|
|
3372
|
+
lexicon.CONTRIBUTOR_INFORMATION_LEXICON_JSON,
|
|
3373
|
+
lexicon.EVALUATION_LEXICON_JSON,
|
|
3374
|
+
lexicon.ATTACHMENT_LEXICON_JSON,
|
|
3375
|
+
lexicon.MEASUREMENT_LEXICON_JSON,
|
|
3376
|
+
lexicon.RIGHTS_LEXICON_JSON,
|
|
3377
|
+
lexicon.ACTOR_PROFILE_LEXICON_JSON,
|
|
3378
|
+
lexicon.BADGE_AWARD_LEXICON_JSON,
|
|
3379
|
+
lexicon.BADGE_DEFINITION_LEXICON_JSON,
|
|
3380
|
+
lexicon.BADGE_RESPONSE_LEXICON_JSON,
|
|
3381
|
+
lexicon.FUNDING_RECEIPT_LEXICON_JSON,
|
|
3382
|
+
lexicon.WORK_SCOPE_TAG_LEXICON_JSON,
|
|
3383
|
+
];
|
|
3384
|
+
/**
|
|
3385
|
+
* Collection NSIDs (Namespaced Identifiers) for hypercert records.
|
|
3386
|
+
*
|
|
3387
|
+
* Use these constants when performing record operations to ensure
|
|
3388
|
+
* correct collection names.
|
|
3389
|
+
*/
|
|
3390
|
+
const HYPERCERT_COLLECTIONS = {
|
|
3184
3391
|
/**
|
|
3185
|
-
*
|
|
3186
|
-
*
|
|
3187
|
-
* @param agent - AT Protocol Agent for making API calls
|
|
3188
|
-
* @param repoDid - DID of the repository/user
|
|
3189
|
-
* @param blobs - Blob operations for uploading images
|
|
3190
|
-
*
|
|
3191
|
-
* @internal
|
|
3392
|
+
* Main hypercert claim record collection.
|
|
3192
3393
|
*/
|
|
3193
|
-
|
|
3394
|
+
CLAIM: lexicon.ACTIVITY_NSID,
|
|
3395
|
+
/**
|
|
3396
|
+
* Rights record collection.
|
|
3397
|
+
*/
|
|
3398
|
+
RIGHTS: lexicon.RIGHTS_NSID,
|
|
3399
|
+
/**
|
|
3400
|
+
* Location record collection (shared certified lexicon).
|
|
3401
|
+
*/
|
|
3402
|
+
LOCATION: lexicon.LOCATION_NSID,
|
|
3403
|
+
/**
|
|
3404
|
+
* Contribution details record collection.
|
|
3405
|
+
* For storing details about a specific contribution (role, description, timeframe).
|
|
3406
|
+
*/
|
|
3407
|
+
CONTRIBUTION_DETAILS: lexicon.CONTRIBUTION_DETAILS_NSID,
|
|
3408
|
+
/**
|
|
3409
|
+
* Contributor information record collection.
|
|
3410
|
+
* For storing contributor profile information (identifier, displayName, image).
|
|
3411
|
+
*/
|
|
3412
|
+
CONTRIBUTOR_INFORMATION: lexicon.CONTRIBUTOR_INFORMATION_NSID,
|
|
3413
|
+
/**
|
|
3414
|
+
* Measurement record collection.
|
|
3415
|
+
*/
|
|
3416
|
+
MEASUREMENT: lexicon.MEASUREMENT_NSID,
|
|
3417
|
+
/**
|
|
3418
|
+
* Evaluation record collection.
|
|
3419
|
+
*/
|
|
3420
|
+
EVALUATION: lexicon.EVALUATION_NSID,
|
|
3421
|
+
/**
|
|
3422
|
+
* Attachment record collection.
|
|
3423
|
+
*/
|
|
3424
|
+
ATTACHMENT: lexicon.ATTACHMENT_NSID,
|
|
3425
|
+
/**
|
|
3426
|
+
* Collection record collection (groups of hypercerts).
|
|
3427
|
+
* Projects are now collections with type='project'.
|
|
3428
|
+
*/
|
|
3429
|
+
COLLECTION: lexicon.COLLECTION_NSID,
|
|
3430
|
+
/**
|
|
3431
|
+
* Badge award record collection.
|
|
3432
|
+
*/
|
|
3433
|
+
BADGE_AWARD: lexicon.BADGE_AWARD_NSID,
|
|
3434
|
+
/**
|
|
3435
|
+
* Badge definition record collection.
|
|
3436
|
+
*/
|
|
3437
|
+
BADGE_DEFINITION: lexicon.BADGE_DEFINITION_NSID,
|
|
3438
|
+
/**
|
|
3439
|
+
* Badge response record collection.
|
|
3440
|
+
*/
|
|
3441
|
+
BADGE_RESPONSE: lexicon.BADGE_RESPONSE_NSID,
|
|
3442
|
+
/**
|
|
3443
|
+
* Funding receipt record collection.
|
|
3444
|
+
*/
|
|
3445
|
+
FUNDING_RECEIPT: lexicon.FUNDING_RECEIPT_NSID,
|
|
3446
|
+
/**
|
|
3447
|
+
* Work scope tag record collection.
|
|
3448
|
+
* For defining reusable work scope atoms.
|
|
3449
|
+
*/
|
|
3450
|
+
WORK_SCOPE_TAG: lexicon.WORK_SCOPE_TAG_NSID,
|
|
3451
|
+
/**
|
|
3452
|
+
* Bluesky profile collection (app.bsky.actor.profile).
|
|
3453
|
+
*/
|
|
3454
|
+
BSKY_PROFILE: "app.bsky.actor.profile",
|
|
3455
|
+
/**
|
|
3456
|
+
* Certified profile collection (app.certified.actor.profile).
|
|
3457
|
+
*/
|
|
3458
|
+
CERTIFIED_PROFILE: lexicon.ACTOR_PROFILE_NSID,
|
|
3459
|
+
};
|
|
3460
|
+
|
|
3461
|
+
/**
|
|
3462
|
+
* Blob URL Utilities
|
|
3463
|
+
*
|
|
3464
|
+
* Utilities for constructing AT Protocol blob URLs and extracting image references
|
|
3465
|
+
* from Hypercert image records.
|
|
3466
|
+
*
|
|
3467
|
+
* @remarks
|
|
3468
|
+
*
|
|
3469
|
+
* ## Why these utilities exist
|
|
3470
|
+
*
|
|
3471
|
+
* AT Protocol stores blobs (images, videos, etc.) on PDS servers and returns blob objects
|
|
3472
|
+
* containing a CID (Content Identifier) and mimetype. To make it easier for SDK users to
|
|
3473
|
+
* consume these blobs, we provide utilities to:
|
|
3474
|
+
*
|
|
3475
|
+
* 1. **Convert blob references to URLs**: Transform CID + DID + PDS into a direct URL
|
|
3476
|
+
* 2. **Extract image references**: Handle both blob-based images (CID) and direct URIs
|
|
3477
|
+
*
|
|
3478
|
+
* This allows users to consume images directly without manual URL construction:
|
|
3479
|
+
* This can and will be reused across any response that has to return a blob.
|
|
3480
|
+
*
|
|
3481
|
+
*
|
|
3482
|
+
* @packageDocumentation
|
|
3483
|
+
*/
|
|
3484
|
+
/**
|
|
3485
|
+
* Constructs a URL for retrieving a blob from an AT Protocol server.
|
|
3486
|
+
*
|
|
3487
|
+
* @param pdsUrl - The PDS/server base URL (e.g., "https://pds1.certified.app")
|
|
3488
|
+
* @param did - The DID of the repository owner
|
|
3489
|
+
* @param cid - The Content Identifier (CID) of the blob
|
|
3490
|
+
* @returns Full blob URL
|
|
3491
|
+
*
|
|
3492
|
+
* @throws {Error} If any parameter is empty or invalid
|
|
3493
|
+
*
|
|
3494
|
+
* @example
|
|
3495
|
+
* ```typescript
|
|
3496
|
+
* const url = getBlobUrl(
|
|
3497
|
+
* "https://pds1.certified.app",
|
|
3498
|
+
* "did:plc:r5p2aletd4fegsklphgiog3s",
|
|
3499
|
+
* "bafkreieie3unmfnzt6j7w2y3zkkcjhisvjtg3au5myonvpuyel6ecau52q"
|
|
3500
|
+
* );
|
|
3501
|
+
* // Returns: "https://pds1.certified.app/xrpc/com.atproto.sync.getBlob?did=did:plc:r5p2aletd4fegsklphgiog3s&cid=bafkreieie3unmfnzt6j7w2y3zkkcjhisvjtg3au5myonvpuyel6ecau52q"
|
|
3502
|
+
* ```
|
|
3503
|
+
*
|
|
3504
|
+
* @public
|
|
3505
|
+
*/
|
|
3506
|
+
function getBlobUrl(pdsUrl, did, cid) {
|
|
3507
|
+
if (!pdsUrl || typeof pdsUrl !== "string") {
|
|
3508
|
+
throw new Error("pdsUrl must be a non-empty string");
|
|
3509
|
+
}
|
|
3510
|
+
if (!did || typeof did !== "string") {
|
|
3511
|
+
throw new Error("did must be a non-empty string");
|
|
3512
|
+
}
|
|
3513
|
+
if (!cid || typeof cid !== "string") {
|
|
3514
|
+
throw new Error("cid must be a non-empty string");
|
|
3515
|
+
}
|
|
3516
|
+
const normalizedPdsUrl = pdsUrl.replace(/\/$/, "");
|
|
3517
|
+
return `${normalizedPdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`;
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Extracts the CID or URI from a Hypercert image record.
|
|
3521
|
+
*
|
|
3522
|
+
* Handles all Hypercert image formats:
|
|
3523
|
+
* - `smallImage`: Avatar/thumbnail format with blob reference - returns CID
|
|
3524
|
+
* - `largeImage`: Banner/cover format with blob reference - returns CID
|
|
3525
|
+
* - `uri`: Direct URL - returns the URI string
|
|
3526
|
+
*
|
|
3527
|
+
* @param image - Hypercert image record
|
|
3528
|
+
* @returns CID string if image contains a blob reference, URI string if uri format, undefined otherwise
|
|
3529
|
+
*
|
|
3530
|
+
* @example Blob format
|
|
3531
|
+
* ```typescript
|
|
3532
|
+
* const smallImage = {
|
|
3533
|
+
* $type: "org.hypercerts.defs#smallImage",
|
|
3534
|
+
* image: {
|
|
3535
|
+
* $type: "blob",
|
|
3536
|
+
* ref: { $link: "bafyrei123" },
|
|
3537
|
+
* mimeType: "image/png",
|
|
3538
|
+
* size: 1000
|
|
3539
|
+
* }
|
|
3540
|
+
* };
|
|
3541
|
+
*
|
|
3542
|
+
* const cid = extractCidFromImage(smallImage);
|
|
3543
|
+
* // Returns: "bafyrei123"
|
|
3544
|
+
* ```
|
|
3545
|
+
*
|
|
3546
|
+
* @example URI format
|
|
3547
|
+
* ```typescript
|
|
3548
|
+
* const uriImage = {
|
|
3549
|
+
* $type: "org.hypercerts.defs#uri",
|
|
3550
|
+
* uri: "https://example.com/image.jpg"
|
|
3551
|
+
* };
|
|
3552
|
+
*
|
|
3553
|
+
* const uri = extractCidFromImage(uriImage);
|
|
3554
|
+
* // Returns: "https://example.com/image.jpg"
|
|
3555
|
+
* ```
|
|
3556
|
+
*
|
|
3557
|
+
* @public
|
|
3558
|
+
*/
|
|
3559
|
+
function extractCidFromImage(image) {
|
|
3560
|
+
if (!image || typeof image !== "object") {
|
|
3561
|
+
return undefined;
|
|
3562
|
+
}
|
|
3563
|
+
// Check $type to determine format
|
|
3564
|
+
const imageType = image.$type;
|
|
3565
|
+
// If it's a URI format, return the URI string directly
|
|
3566
|
+
if (imageType === "org.hypercerts.defs#uri") {
|
|
3567
|
+
const uri = image.uri;
|
|
3568
|
+
if (uri && typeof uri === "string") {
|
|
3569
|
+
return uri;
|
|
3570
|
+
}
|
|
3571
|
+
return undefined;
|
|
3572
|
+
}
|
|
3573
|
+
if (imageType === "org.hypercerts.defs#smallImage" || imageType === "org.hypercerts.defs#largeImage") {
|
|
3574
|
+
// Blob format: image.image.ref.$link
|
|
3575
|
+
const imageData = image.image;
|
|
3576
|
+
if (imageData && typeof imageData === "object") {
|
|
3577
|
+
const ref = imageData.ref;
|
|
3578
|
+
if (ref) {
|
|
3579
|
+
return ref.toString();
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
return undefined;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
/**
|
|
3587
|
+
* ProfileOperationsImpl - User profile operations supporting dual profiles.
|
|
3588
|
+
*
|
|
3589
|
+
* This module provides operations for managing AT Protocol profiles:
|
|
3590
|
+
* - Bluesky profiles (app.bsky.actor.profile) - Simple profiles with CDN images
|
|
3591
|
+
* - Certified profiles (app.certified.actor.profile) - Hypercerts profiles with pronouns/website
|
|
3592
|
+
*
|
|
3593
|
+
* @packageDocumentation
|
|
3594
|
+
*/
|
|
3595
|
+
const BSKY_PROFILE_NSID = HYPERCERT_COLLECTIONS.BSKY_PROFILE;
|
|
3596
|
+
const CERTIFIED_PROFILE_NSID = HYPERCERT_COLLECTIONS.CERTIFIED_PROFILE;
|
|
3597
|
+
/** Profile record key (always "self" for the user's own profile) */
|
|
3598
|
+
const PROFILE_RKEY = "self";
|
|
3599
|
+
/**
|
|
3600
|
+
* Implementation of profile operations supporting dual profiles.
|
|
3601
|
+
*
|
|
3602
|
+
* This class provides operations for both Bluesky and Certified profiles:
|
|
3603
|
+
* - Bluesky profiles: Simple AT Protocol profiles with avatar/banner as CDN URLs
|
|
3604
|
+
* - Certified profiles: Hypercerts profiles hypercerts image format
|
|
3605
|
+
*
|
|
3606
|
+
* @remarks
|
|
3607
|
+
* This class is typically not instantiated directly. Access it through
|
|
3608
|
+
* {@link Repository.profile}.
|
|
3609
|
+
*
|
|
3610
|
+
* **Profile Types**:
|
|
3611
|
+
* - `app.bsky.actor.profile`: Standard Bluesky profile, images are simple blob refs
|
|
3612
|
+
* - `app.certified.actor.profile`: Hypercerts profile, images wrapped in smallImage/largeImage. Omits some bsky profile properties like pinnedPost labels etc
|
|
3613
|
+
*
|
|
3614
|
+
* @example
|
|
3615
|
+
* ```typescript
|
|
3616
|
+
* // Get Bluesky profile
|
|
3617
|
+
* const bskyProfile = await repo.profile.getBskyProfile();
|
|
3618
|
+
* console.log(bskyProfile.displayName);
|
|
3619
|
+
* console.log(bskyProfile.avatar); // CDN URL
|
|
3620
|
+
*
|
|
3621
|
+
* // Get Certified profile
|
|
3622
|
+
* const certifiedProfile = await repo.profile.getCertifiedProfile();
|
|
3623
|
+
* console.log(certifiedProfile.pronouns); // "she/her"
|
|
3624
|
+
* console.log(certifiedProfile.avatar); // Blob URL
|
|
3625
|
+
*
|
|
3626
|
+
* // Create/update profiles
|
|
3627
|
+
* await repo.profile.createBskyProfile({ displayName: "Alice" });
|
|
3628
|
+
* await repo.profile.updateBskyProfile({ description: "New bio" });
|
|
3629
|
+
*
|
|
3630
|
+
* await repo.profile.createCertifiedProfile({
|
|
3631
|
+
* displayName: "Alice",
|
|
3632
|
+
* pronouns: "she/her",
|
|
3633
|
+
* });
|
|
3634
|
+
* ```
|
|
3635
|
+
*
|
|
3636
|
+
* @internal
|
|
3637
|
+
*/
|
|
3638
|
+
class ProfileOperationsImpl {
|
|
3639
|
+
/**
|
|
3640
|
+
* Creates a new ProfileOperationsImpl.
|
|
3641
|
+
*
|
|
3642
|
+
* @param agent - AT Protocol Agent for making API calls
|
|
3643
|
+
* @param repoDid - DID of the repository/user
|
|
3644
|
+
* @param blobs - Blob operations for uploading images
|
|
3645
|
+
* @param pdsUrl - PDS URL for constructing blob URLs
|
|
3646
|
+
*
|
|
3647
|
+
* @internal
|
|
3648
|
+
*/
|
|
3649
|
+
constructor(agent, repoDid, blobs, pdsUrl) {
|
|
3194
3650
|
this.agent = agent;
|
|
3195
3651
|
this.repoDid = repoDid;
|
|
3196
3652
|
this.blobs = blobs;
|
|
3653
|
+
this.pdsUrl = pdsUrl;
|
|
3197
3654
|
}
|
|
3198
3655
|
/**
|
|
3199
|
-
*
|
|
3656
|
+
* Converts a Hypercert image record to a URL string.
|
|
3657
|
+
*
|
|
3658
|
+
* - URI format: returns the URI string directly
|
|
3659
|
+
* - Blob format (smallImage/largeImage): constructs blob URL using PDS
|
|
3660
|
+
*
|
|
3661
|
+
* @param image - Hypercert image record
|
|
3662
|
+
* @returns URL string
|
|
3663
|
+
* @throws {Error} If image format is invalid or blob CID cannot be extracted
|
|
3200
3664
|
*
|
|
3201
3665
|
* @internal
|
|
3202
3666
|
*/
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
delete result[field];
|
|
3667
|
+
imageToUrl(image) {
|
|
3668
|
+
const result = extractCidFromImage(image);
|
|
3669
|
+
if (!result) {
|
|
3670
|
+
throw new Error("Unable to extract CID or URI from image record");
|
|
3208
3671
|
}
|
|
3209
|
-
|
|
3210
|
-
result
|
|
3672
|
+
if (isValidUri(result)) {
|
|
3673
|
+
return result;
|
|
3211
3674
|
}
|
|
3675
|
+
// Otherwise, it's a CID - construct blob URL
|
|
3676
|
+
return getBlobUrl(this.pdsUrl, this.repoDid, result);
|
|
3212
3677
|
}
|
|
3213
3678
|
/**
|
|
3214
|
-
* Applies
|
|
3679
|
+
* Applies an image field (avatar/banner) with format-specific wrapping.
|
|
3680
|
+
*
|
|
3681
|
+
* - null: removes the field
|
|
3682
|
+
* - undefined: no change
|
|
3683
|
+
* - Blob: uploads and wraps according to collection format
|
|
3684
|
+
*
|
|
3685
|
+
* @param result - The profile record being built
|
|
3686
|
+
* @param field - Field name ("avatar" or "banner")
|
|
3687
|
+
* @param value - Blob to upload, null to remove, or undefined to skip
|
|
3688
|
+
* @param collection - Profile collection NSID (determines image wrapping format)
|
|
3215
3689
|
*
|
|
3216
3690
|
* @internal
|
|
3217
3691
|
*/
|
|
3218
|
-
async
|
|
3219
|
-
if (
|
|
3692
|
+
async applyImageField(result, field, value, collection) {
|
|
3693
|
+
if (value === undefined)
|
|
3220
3694
|
return;
|
|
3221
|
-
if (
|
|
3695
|
+
if (value === null) {
|
|
3222
3696
|
delete result[field];
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
const blobRef = await this.blobs.upload(value);
|
|
3700
|
+
// Bsky profiles use simple blob refs, Certified profiles wrap in smallImage/largeImage
|
|
3701
|
+
if (collection === BSKY_PROFILE_NSID) {
|
|
3702
|
+
result[field] = blobRef;
|
|
3223
3703
|
}
|
|
3224
3704
|
else {
|
|
3225
|
-
const
|
|
3226
|
-
result[field] =
|
|
3705
|
+
const isLargeImage = field === "banner";
|
|
3706
|
+
result[field] = {
|
|
3707
|
+
$type: isLargeImage ? "org.hypercerts.defs#largeImage" : "org.hypercerts.defs#smallImage",
|
|
3708
|
+
image: blobRef,
|
|
3709
|
+
};
|
|
3227
3710
|
}
|
|
3228
3711
|
}
|
|
3229
3712
|
/**
|
|
3230
|
-
*
|
|
3231
|
-
*
|
|
3232
|
-
* Ensures $type and createdAt are always present on the record, using
|
|
3233
|
-
* nullish coalescing to allow callers to override defaults.
|
|
3713
|
+
* Validates a profile record against the appropriate lexicon schema.
|
|
3234
3714
|
*
|
|
3715
|
+
* @param profile - The profile record to validate
|
|
3716
|
+
* @param collection - Profile collection NSID (determines validation schema)
|
|
3717
|
+
* @throws {ValidationError} If validation fails
|
|
3235
3718
|
* @internal
|
|
3236
3719
|
*/
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
this.applySimpleField(result, "displayName", params.displayName);
|
|
3243
|
-
this.applySimpleField(result, "description", params.description);
|
|
3244
|
-
this.applySimpleField(result, "website", params.website);
|
|
3245
|
-
await this.applyBlobField(result, "avatar", params.avatar);
|
|
3246
|
-
await this.applyBlobField(result, "banner", params.banner);
|
|
3247
|
-
return result;
|
|
3248
|
-
}
|
|
3249
|
-
/**
|
|
3250
|
-
* Gets the repository's profile.
|
|
3251
|
-
*
|
|
3252
|
-
* @returns Promise resolving to profile data
|
|
3253
|
-
* @throws {@link NetworkError} if the profile cannot be fetched
|
|
3254
|
-
*
|
|
3255
|
-
* @remarks
|
|
3256
|
-
* This method fetches the full profile using the `getProfile` API,
|
|
3257
|
-
* which includes resolved information like follower counts on some
|
|
3258
|
-
* servers. For hypercerts SDK usage, the basic profile fields are
|
|
3259
|
-
* returned.
|
|
3260
|
-
*
|
|
3261
|
-
* **Note**: The `website` field may not be available on all AT Protocol
|
|
3262
|
-
* servers. Standard Bluesky profiles don't include this field.
|
|
3263
|
-
*
|
|
3264
|
-
* @example
|
|
3265
|
-
* ```typescript
|
|
3266
|
-
* const profile = await repo.profile.get();
|
|
3267
|
-
*
|
|
3268
|
-
* console.log(`Handle: @${profile.handle}`);
|
|
3269
|
-
* console.log(`Name: ${profile.displayName || "(not set)"}`);
|
|
3270
|
-
* console.log(`Bio: ${profile.description || "(no bio)"}`);
|
|
3271
|
-
*
|
|
3272
|
-
* if (profile.avatar) {
|
|
3273
|
-
* // Avatar is a URL or blob reference
|
|
3274
|
-
* console.log(`Avatar: ${profile.avatar}`);
|
|
3275
|
-
* }
|
|
3276
|
-
* ```
|
|
3277
|
-
*/
|
|
3278
|
-
async get() {
|
|
3279
|
-
try {
|
|
3280
|
-
const result = await this.agent.getProfile({ actor: this.repoDid });
|
|
3281
|
-
if (!result.success) {
|
|
3282
|
-
throw new NetworkError("Failed to get profile");
|
|
3720
|
+
validateProfileRecord(profile, collection) {
|
|
3721
|
+
if (collection === CERTIFIED_PROFILE_NSID) {
|
|
3722
|
+
const validation = lexicon.validate(profile, CERTIFIED_PROFILE_NSID, "main", false);
|
|
3723
|
+
if (!validation.success) {
|
|
3724
|
+
throw new ValidationError(`Invalid profile record: ${validation.error?.message}`);
|
|
3283
3725
|
}
|
|
3284
|
-
return {
|
|
3285
|
-
handle: result.data.handle,
|
|
3286
|
-
displayName: result.data.displayName,
|
|
3287
|
-
description: result.data.description,
|
|
3288
|
-
avatar: result.data.avatar,
|
|
3289
|
-
banner: result.data.banner,
|
|
3290
|
-
// Note: website may not be available in standard profile
|
|
3291
|
-
};
|
|
3292
3726
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3727
|
+
if (collection === BSKY_PROFILE_NSID) {
|
|
3728
|
+
const validation = api.AppBskyActorProfile.validateMain(profile);
|
|
3729
|
+
if (!validation.success) {
|
|
3730
|
+
throw new ValidationError(`Invalid profile record: ${validation.error?.message}`);
|
|
3731
|
+
}
|
|
3297
3732
|
}
|
|
3298
3733
|
}
|
|
3299
3734
|
/**
|
|
3300
|
-
* Creates a
|
|
3735
|
+
* Creates a profile record with lexicon validation.
|
|
3301
3736
|
*
|
|
3302
|
-
* @param
|
|
3737
|
+
* @param collection - NSID of the collection (Bsky or Certified profile)
|
|
3738
|
+
* @param params - Profile creation parameters
|
|
3303
3739
|
* @returns Promise resolving to create result with URI and CID
|
|
3304
|
-
* @throws {
|
|
3305
|
-
*
|
|
3306
|
-
* @
|
|
3307
|
-
* Use this method when no profile exists yet. If a profile already exists,
|
|
3308
|
-
* use {@link update} instead.
|
|
3309
|
-
*
|
|
3310
|
-
* **Image Handling**: When providing `avatar` or `banner` as a Blob,
|
|
3311
|
-
* the image is automatically uploaded and the blob reference is stored
|
|
3312
|
-
* in the profile.
|
|
3313
|
-
*
|
|
3314
|
-
* @example Create a basic profile
|
|
3315
|
-
* ```typescript
|
|
3316
|
-
* await repo.profile.create({
|
|
3317
|
-
* displayName: "Alice",
|
|
3318
|
-
* description: "Building impact certificates",
|
|
3319
|
-
* });
|
|
3320
|
-
* ```
|
|
3321
|
-
*
|
|
3322
|
-
* @example Create a profile with avatar
|
|
3323
|
-
* ```typescript
|
|
3324
|
-
* const avatarBlob = new Blob([avatarData], { type: "image/png" });
|
|
3325
|
-
* await repo.profile.create({
|
|
3326
|
-
* displayName: "Alice",
|
|
3327
|
-
* description: "Building impact certificates",
|
|
3328
|
-
* avatar: avatarBlob,
|
|
3329
|
-
* });
|
|
3330
|
-
* ```
|
|
3740
|
+
* @throws {ValidationError} if validation fails
|
|
3741
|
+
* @throws {NetworkError} if creation fails
|
|
3742
|
+
* @internal
|
|
3331
3743
|
*/
|
|
3332
|
-
async
|
|
3744
|
+
async createProfileRecord(collection, params) {
|
|
3333
3745
|
try {
|
|
3334
|
-
const
|
|
3335
|
-
const
|
|
3746
|
+
const { avatar, banner, $type, createdAt, ...otherFields } = params;
|
|
3747
|
+
const profile = {
|
|
3748
|
+
$type: $type ?? collection,
|
|
3749
|
+
createdAt: createdAt ?? new Date().toISOString(),
|
|
3750
|
+
...otherFields,
|
|
3751
|
+
};
|
|
3752
|
+
await this.applyImageField(profile, "avatar", avatar, collection);
|
|
3753
|
+
await this.applyImageField(profile, "banner", banner, collection);
|
|
3754
|
+
// Validate profile record against lexicon schema
|
|
3755
|
+
this.validateProfileRecord(profile, collection);
|
|
3756
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
3336
3757
|
repo: this.repoDid,
|
|
3337
|
-
collection
|
|
3338
|
-
rkey:
|
|
3758
|
+
collection,
|
|
3759
|
+
rkey: PROFILE_RKEY,
|
|
3339
3760
|
record: profile,
|
|
3340
|
-
};
|
|
3341
|
-
const result = await this.agent.com.atproto.repo.createRecord(createParams);
|
|
3761
|
+
});
|
|
3342
3762
|
if (!result.success) {
|
|
3343
3763
|
throw new NetworkError("Failed to create profile");
|
|
3344
3764
|
}
|
|
3345
3765
|
return { uri: result.data.uri, cid: result.data.cid };
|
|
3346
3766
|
}
|
|
3347
3767
|
catch (error) {
|
|
3348
|
-
if (error instanceof NetworkError)
|
|
3768
|
+
if (error instanceof NetworkError || error instanceof ValidationError)
|
|
3349
3769
|
throw error;
|
|
3350
3770
|
throw new NetworkError(`Failed to create profile: ${error instanceof Error ? error.message : "Unknown error"}`, error);
|
|
3351
3771
|
}
|
|
3352
3772
|
}
|
|
3353
3773
|
/**
|
|
3354
|
-
* Updates
|
|
3355
|
-
*
|
|
3356
|
-
* @param params - Fields to update. Pass `null` to remove a field.
|
|
3357
|
-
* Omitted fields are preserved from the existing profile.
|
|
3358
|
-
* @returns Promise resolving to update result with new URI and CID
|
|
3359
|
-
* @throws {@link NetworkError} if the update fails
|
|
3360
|
-
*
|
|
3361
|
-
* @remarks
|
|
3362
|
-
* This method performs a read-modify-write operation:
|
|
3363
|
-
* 1. Fetches the existing profile record
|
|
3364
|
-
* 2. Merges in the provided updates
|
|
3365
|
-
* 3. Writes the updated profile back
|
|
3366
|
-
*
|
|
3367
|
-
* **Image Handling**: When providing `avatar` or `banner` as a Blob,
|
|
3368
|
-
* the image is automatically uploaded and the blob reference is stored
|
|
3369
|
-
* in the profile.
|
|
3370
|
-
*
|
|
3371
|
-
* **Field Removal**: Pass `null` to explicitly remove a field. Omitting
|
|
3372
|
-
* a field (not including it in params) preserves the existing value.
|
|
3373
|
-
*
|
|
3374
|
-
* @example Update display name and bio
|
|
3375
|
-
* ```typescript
|
|
3376
|
-
* await repo.profile.update({
|
|
3377
|
-
* displayName: "Alice",
|
|
3378
|
-
* description: "Building impact certificates",
|
|
3379
|
-
* });
|
|
3380
|
-
* ```
|
|
3381
|
-
*
|
|
3382
|
-
* @example Update avatar image
|
|
3383
|
-
* ```typescript
|
|
3384
|
-
* // From a file input
|
|
3385
|
-
* const file = document.getElementById("avatar").files[0];
|
|
3386
|
-
* await repo.profile.update({ avatar: file });
|
|
3387
|
-
*
|
|
3388
|
-
* // From raw data
|
|
3389
|
-
* const response = await fetch("https://example.com/my-avatar.png");
|
|
3390
|
-
* const blob = await response.blob();
|
|
3391
|
-
* await repo.profile.update({ avatar: blob });
|
|
3392
|
-
* ```
|
|
3393
|
-
*
|
|
3394
|
-
* @example Remove description
|
|
3395
|
-
* ```typescript
|
|
3396
|
-
* // Removes the description field entirely
|
|
3397
|
-
* await repo.profile.update({ description: null });
|
|
3398
|
-
* ```
|
|
3399
|
-
*
|
|
3400
|
-
* @example Multiple updates at once
|
|
3401
|
-
* ```typescript
|
|
3402
|
-
* const newAvatar = new Blob([avatarData], { type: "image/png" });
|
|
3403
|
-
* const newBanner = new Blob([bannerData], { type: "image/jpeg" });
|
|
3774
|
+
* Updates a profile record with proper null handling and validation.
|
|
3404
3775
|
*
|
|
3405
|
-
*
|
|
3406
|
-
*
|
|
3407
|
-
*
|
|
3408
|
-
*
|
|
3409
|
-
*
|
|
3410
|
-
*
|
|
3411
|
-
* ```
|
|
3776
|
+
* @param collection - NSID of the collection (Bsky or Certified profile)
|
|
3777
|
+
* @param params - Profile update parameters (partial, with null for deletions)
|
|
3778
|
+
* @returns Promise resolving to update result with URI and CID
|
|
3779
|
+
* @throws {NetworkError} if profile not found or update fails
|
|
3780
|
+
* @throws {ValidationError} if validation fails
|
|
3781
|
+
* @internal
|
|
3412
3782
|
*/
|
|
3413
|
-
async
|
|
3783
|
+
async updateProfileRecord(collection, params) {
|
|
3414
3784
|
try {
|
|
3415
|
-
|
|
3416
|
-
const getParams = {
|
|
3785
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
3417
3786
|
repo: this.repoDid,
|
|
3418
|
-
collection
|
|
3419
|
-
rkey:
|
|
3420
|
-
};
|
|
3421
|
-
const existing = await this.agent.com.atproto.repo.getRecord(getParams);
|
|
3787
|
+
collection,
|
|
3788
|
+
rkey: PROFILE_RKEY,
|
|
3789
|
+
});
|
|
3422
3790
|
if (!existing.success) {
|
|
3423
|
-
throw new NetworkError("Profile not found
|
|
3791
|
+
throw new NetworkError("Profile not found");
|
|
3792
|
+
}
|
|
3793
|
+
const { avatar, banner, ...otherFields } = params;
|
|
3794
|
+
const updatedProfile = {
|
|
3795
|
+
...existing.data.value,
|
|
3796
|
+
};
|
|
3797
|
+
// Apply non-image field updates, handling null deletions
|
|
3798
|
+
for (const [key, value] of Object.entries(otherFields)) {
|
|
3799
|
+
if (value === null) {
|
|
3800
|
+
delete updatedProfile[key];
|
|
3801
|
+
}
|
|
3802
|
+
else if (value !== undefined) {
|
|
3803
|
+
updatedProfile[key] = value;
|
|
3804
|
+
}
|
|
3424
3805
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3806
|
+
await this.applyImageField(updatedProfile, "avatar", avatar, collection);
|
|
3807
|
+
await this.applyImageField(updatedProfile, "banner", banner, collection);
|
|
3808
|
+
// Validate updated record against lexicon schema
|
|
3809
|
+
this.validateProfileRecord(updatedProfile, collection);
|
|
3810
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
3428
3811
|
repo: this.repoDid,
|
|
3429
|
-
collection
|
|
3430
|
-
rkey:
|
|
3812
|
+
collection,
|
|
3813
|
+
rkey: PROFILE_RKEY,
|
|
3431
3814
|
record: updatedProfile,
|
|
3432
|
-
};
|
|
3433
|
-
const result = await this.agent.com.atproto.repo.putRecord(putParams);
|
|
3815
|
+
});
|
|
3434
3816
|
if (!result.success) {
|
|
3435
3817
|
throw new NetworkError("Failed to update profile");
|
|
3436
3818
|
}
|
|
3437
3819
|
return { uri: result.data.uri, cid: result.data.cid };
|
|
3438
3820
|
}
|
|
3439
3821
|
catch (error) {
|
|
3440
|
-
if (error instanceof NetworkError)
|
|
3822
|
+
if (error instanceof NetworkError || error instanceof ValidationError)
|
|
3441
3823
|
throw error;
|
|
3442
3824
|
throw new NetworkError(`Failed to update profile: ${error instanceof Error ? error.message : "Unknown error"}`, error);
|
|
3443
3825
|
}
|
|
3444
3826
|
}
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3827
|
+
/**
|
|
3828
|
+
* Upserts a profile record (creates if missing, updates if exists).
|
|
3829
|
+
*
|
|
3830
|
+
* @param collection - NSID of the collection (Bsky or Certified profile)
|
|
3831
|
+
* @param params - Profile fields to set
|
|
3832
|
+
* @returns Promise resolving to update result with URI and CID
|
|
3833
|
+
* @throws {ValidationError} if validation fails
|
|
3834
|
+
* @throws {NetworkError} if operation fails
|
|
3835
|
+
* @internal
|
|
3836
|
+
*/
|
|
3837
|
+
async upsertProfileRecord(collection, params) {
|
|
3838
|
+
try {
|
|
3839
|
+
// Check if profile exists
|
|
3840
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
3841
|
+
repo: this.repoDid,
|
|
3842
|
+
collection,
|
|
3843
|
+
rkey: PROFILE_RKEY,
|
|
3844
|
+
});
|
|
3845
|
+
if (!existing.success) {
|
|
3846
|
+
return this.createProfileRecord(collection, params);
|
|
3847
|
+
}
|
|
3848
|
+
const { avatar, banner, ...otherFields } = params;
|
|
3849
|
+
const updatedProfile = {
|
|
3850
|
+
...existing.data.value,
|
|
3851
|
+
};
|
|
3852
|
+
for (const [key, value] of Object.entries(otherFields)) {
|
|
3853
|
+
// ignore these since profile has already created
|
|
3854
|
+
if (["$type", "createdAt"].includes(key))
|
|
3855
|
+
continue;
|
|
3856
|
+
if (value === null) {
|
|
3857
|
+
delete updatedProfile[key];
|
|
3858
|
+
}
|
|
3859
|
+
else if (value !== undefined) {
|
|
3860
|
+
updatedProfile[key] = value;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
await this.applyImageField(updatedProfile, "avatar", avatar, collection);
|
|
3864
|
+
await this.applyImageField(updatedProfile, "banner", banner, collection);
|
|
3865
|
+
this.validateProfileRecord(updatedProfile, collection);
|
|
3866
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
3867
|
+
repo: this.repoDid,
|
|
3868
|
+
collection,
|
|
3869
|
+
rkey: PROFILE_RKEY,
|
|
3870
|
+
record: updatedProfile,
|
|
3871
|
+
});
|
|
3872
|
+
if (!result.success) {
|
|
3873
|
+
throw new NetworkError("Failed to upsert profile");
|
|
3874
|
+
}
|
|
3875
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
3876
|
+
}
|
|
3877
|
+
catch (error) {
|
|
3878
|
+
if (error instanceof NetworkError || error instanceof ValidationError)
|
|
3879
|
+
throw error;
|
|
3880
|
+
throw new NetworkError(`Failed to upsert profile: ${error instanceof Error ? error.message : "Unknown error"}`, error);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* Gets Bluesky profile (app.bsky.actor.profile).
|
|
3885
|
+
*
|
|
3886
|
+
* @returns Promise resolving to Bluesky profile data
|
|
3887
|
+
* @throws {NetworkError} If profile cannot be fetched
|
|
3888
|
+
*
|
|
3889
|
+
* @example
|
|
3890
|
+
* ```typescript
|
|
3891
|
+
* const bskyProfile = await repo.profile.getBskyProfile();
|
|
3892
|
+
* console.log(bskyProfile.displayName); // "Alice"
|
|
3893
|
+
* console.log(bskyProfile.avatar); // "https://cdn.bsky.app/..."
|
|
3894
|
+
* ```
|
|
3895
|
+
*/
|
|
3896
|
+
async getBskyProfile() {
|
|
3897
|
+
try {
|
|
3898
|
+
const profileResult = await this.agent.getProfile({ actor: this.repoDid });
|
|
3899
|
+
if (!profileResult.success) {
|
|
3900
|
+
throw new NetworkError("Failed to get Bluesky profile");
|
|
3901
|
+
}
|
|
3902
|
+
return profileResult.data;
|
|
3903
|
+
}
|
|
3904
|
+
catch (error) {
|
|
3905
|
+
if (error instanceof NetworkError)
|
|
3906
|
+
throw error;
|
|
3907
|
+
throw new NetworkError(`Failed to get Bluesky profile: ${error instanceof Error ? error.message : "Unknown error"}`, error);
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3911
|
+
* Gets Certified profile (app.certified.actor.profile).
|
|
3912
|
+
*
|
|
3913
|
+
* Returns the profile record with avatar and banner converted to blob URLs.
|
|
3914
|
+
* Includes the user's handle fetched from getProfile(). If getProfile() fails,
|
|
3915
|
+
* handle is set to empty string.
|
|
3916
|
+
*
|
|
3917
|
+
* @returns Promise resolving to Certified profile data, or null if no profile exists
|
|
3918
|
+
* @throws {NetworkError} If profile fetch fails due to network/server issues
|
|
3919
|
+
*
|
|
3920
|
+
* @example
|
|
3921
|
+
* ```typescript
|
|
3922
|
+
* const certifiedProfile = await repo.profile.getCertifiedProfile();
|
|
3923
|
+
* if (certifiedProfile) {
|
|
3924
|
+
* console.log(certifiedProfile.displayName); // "Alice"
|
|
3925
|
+
* console.log(certifiedProfile.pronouns); // "she/her"
|
|
3926
|
+
* console.log(certifiedProfile.avatar); // "https://pds.../xrpc/..."
|
|
3927
|
+
* } else {
|
|
3928
|
+
* console.log("User hasn't created a certified profile yet");
|
|
3929
|
+
* }
|
|
3930
|
+
* ```
|
|
3931
|
+
*/
|
|
3932
|
+
async getCertifiedProfile() {
|
|
3933
|
+
try {
|
|
3934
|
+
// Fetch handle from Bluesky profile (non-blocking)
|
|
3935
|
+
let handle = "";
|
|
3936
|
+
try {
|
|
3937
|
+
const profileResult = await this.agent.getProfile({ actor: this.repoDid });
|
|
3938
|
+
if (profileResult.success) {
|
|
3939
|
+
handle = profileResult.data.handle;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
catch {
|
|
3943
|
+
// Ignore error, use empty string for handle
|
|
3944
|
+
handle = "";
|
|
3945
|
+
}
|
|
3946
|
+
// Fetch certified profile record
|
|
3947
|
+
const recordResult = await this.agent.com.atproto.repo.getRecord({
|
|
3948
|
+
repo: this.repoDid,
|
|
3949
|
+
collection: CERTIFIED_PROFILE_NSID,
|
|
3950
|
+
rkey: PROFILE_RKEY,
|
|
3951
|
+
});
|
|
3952
|
+
if (!recordResult.success) {
|
|
3953
|
+
return null;
|
|
3954
|
+
}
|
|
3955
|
+
const profileRecord = recordResult.data.value;
|
|
3956
|
+
let avatar;
|
|
3957
|
+
let banner;
|
|
3958
|
+
if (profileRecord.avatar) {
|
|
3959
|
+
avatar = this.imageToUrl(profileRecord.avatar);
|
|
3960
|
+
}
|
|
3961
|
+
if (profileRecord.banner) {
|
|
3962
|
+
banner = this.imageToUrl(profileRecord.banner);
|
|
3963
|
+
}
|
|
3964
|
+
return {
|
|
3965
|
+
...profileRecord,
|
|
3966
|
+
handle,
|
|
3967
|
+
avatar,
|
|
3968
|
+
banner,
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
catch (error) {
|
|
3972
|
+
// Check for RecordNotFoundError from AT Protocol SDK
|
|
3973
|
+
if (error && typeof error === "object" && "error" in error && error.error === "RecordNotFound") {
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
// Actual network/server errors still throw
|
|
3977
|
+
if (error instanceof NetworkError)
|
|
3978
|
+
throw error;
|
|
3979
|
+
throw new NetworkError(`Failed to get Certified profile: ${error instanceof Error ? error.message : "Unknown error"}`, error);
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Creates Bluesky profile (app.bsky.actor.profile).
|
|
3984
|
+
*
|
|
3985
|
+
* @param params - Profile fields to set
|
|
3986
|
+
* @returns Promise resolving to create result with URI and CID
|
|
3987
|
+
* @throws {NetworkError} If creation fails
|
|
3988
|
+
*
|
|
3989
|
+
* @example
|
|
3990
|
+
* ```typescript
|
|
3991
|
+
* await repo.profile.createBskyProfile({
|
|
3992
|
+
* displayName: "Alice",
|
|
3993
|
+
* description: "Building impact certificates",
|
|
3994
|
+
* });
|
|
3995
|
+
* ```
|
|
3996
|
+
*/
|
|
3997
|
+
async createBskyProfile(params) {
|
|
3998
|
+
return this.createProfileRecord(BSKY_PROFILE_NSID, params);
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Updates Bluesky profile (app.bsky.actor.profile).
|
|
4002
|
+
*
|
|
4003
|
+
* @param params - Fields to update (pass null to remove)
|
|
4004
|
+
* @returns Promise resolving to update result with URI and CID
|
|
4005
|
+
* @throws {NetworkError} If update fails
|
|
4006
|
+
*
|
|
4007
|
+
* @example
|
|
4008
|
+
* ```typescript
|
|
4009
|
+
* await repo.profile.updateBskyProfile({
|
|
4010
|
+
* displayName: "New Name",
|
|
4011
|
+
* description: null, // Remove description
|
|
4012
|
+
* });
|
|
4013
|
+
* ```
|
|
4014
|
+
*/
|
|
4015
|
+
async updateBskyProfile(params) {
|
|
4016
|
+
return this.updateProfileRecord(BSKY_PROFILE_NSID, params);
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* Creates Certified profile (app.certified.actor.profile).
|
|
4020
|
+
*
|
|
4021
|
+
* @param params - Profile fields to set
|
|
4022
|
+
* @returns Promise resolving to create result with URI and CID
|
|
4023
|
+
* @throws {NetworkError} If creation fails
|
|
4024
|
+
*
|
|
4025
|
+
* @example
|
|
4026
|
+
* ```typescript
|
|
4027
|
+
* await repo.profile.createCertifiedProfile({
|
|
4028
|
+
* displayName: "Alice",
|
|
4029
|
+
* description: "Building impact certificates",
|
|
4030
|
+
* pronouns: "she/her",
|
|
4031
|
+
* website: "https://alice.com",
|
|
4032
|
+
* });
|
|
4033
|
+
* ```
|
|
4034
|
+
*/
|
|
4035
|
+
async createCertifiedProfile(params) {
|
|
4036
|
+
return this.createProfileRecord(CERTIFIED_PROFILE_NSID, params);
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Updates Certified profile (app.certified.actor.profile).
|
|
4040
|
+
*
|
|
4041
|
+
* @param params - Fields to update (pass null to remove)
|
|
4042
|
+
* @returns Promise resolving to update result with URI and CID
|
|
4043
|
+
* @throws {NetworkError} If update fails
|
|
4044
|
+
*
|
|
4045
|
+
* @example
|
|
4046
|
+
* ```typescript
|
|
4047
|
+
* await repo.profile.updateCertifiedProfile({
|
|
4048
|
+
* displayName: "New Name",
|
|
4049
|
+
* pronouns: null, // Remove pronouns
|
|
4050
|
+
* });
|
|
4051
|
+
* ```
|
|
4052
|
+
*/
|
|
4053
|
+
async updateCertifiedProfile(params) {
|
|
4054
|
+
return this.updateProfileRecord(CERTIFIED_PROFILE_NSID, params);
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Upserts Bluesky profile (creates if missing, updates if exists).
|
|
4058
|
+
*
|
|
4059
|
+
* Automatically detects whether the profile exists and creates or updates accordingly.
|
|
4060
|
+
* This is the recommended method for most use cases.
|
|
4061
|
+
*
|
|
4062
|
+
* @param params - Profile fields to set
|
|
4063
|
+
* @returns Promise resolving to update result with URI and CID
|
|
4064
|
+
* @throws {NetworkError} If operation fails
|
|
4065
|
+
* @throws {ValidationError} If validation fails
|
|
4066
|
+
*
|
|
4067
|
+
* @example
|
|
4068
|
+
* ```typescript
|
|
4069
|
+
* // Works whether profile exists or not
|
|
4070
|
+
* await repo.profile.upsertBskyProfile({
|
|
4071
|
+
* displayName: "Alice",
|
|
4072
|
+
* description: "Building on AT Protocol",
|
|
4073
|
+
* });
|
|
4074
|
+
* ```
|
|
4075
|
+
*/
|
|
4076
|
+
async upsertBskyProfile(params) {
|
|
4077
|
+
return this.upsertProfileRecord(BSKY_PROFILE_NSID, params);
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Upserts Certified profile (creates if missing, updates if exists).
|
|
4081
|
+
*
|
|
4082
|
+
* Automatically detects whether the profile exists and creates or updates accordingly.
|
|
4083
|
+
* This is the recommended method for most use cases.
|
|
4084
|
+
*
|
|
4085
|
+
* @param params - Profile fields to set
|
|
4086
|
+
* @returns Promise resolving to update result with URI and CID
|
|
4087
|
+
* @throws {NetworkError} If operation fails
|
|
4088
|
+
* @throws {ValidationError} If validation fails
|
|
4089
|
+
*
|
|
4090
|
+
* @example
|
|
4091
|
+
* ```typescript
|
|
4092
|
+
* // Works whether profile exists or not
|
|
4093
|
+
* await repo.profile.upsertCertifiedProfile({
|
|
4094
|
+
* displayName: "Alice",
|
|
4095
|
+
* pronouns: "she/her",
|
|
4096
|
+
* website: "https://alice.com",
|
|
4097
|
+
* });
|
|
4098
|
+
* ```
|
|
4099
|
+
*/
|
|
4100
|
+
async upsertCertifiedProfile(params) {
|
|
4101
|
+
return this.upsertProfileRecord(CERTIFIED_PROFILE_NSID, params);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
/**
|
|
4106
|
+
* Crypto utilities for the SDK.
|
|
4107
|
+
*/
|
|
4108
|
+
/**
|
|
4109
|
+
* Deterministically stringifies an object by sorting keys recursively.
|
|
4110
|
+
* Handles deeply nested objects and null values correctly.
|
|
4111
|
+
*/
|
|
4112
|
+
function stableStringify(obj) {
|
|
4113
|
+
if (obj === undefined || typeof obj === "function" || typeof obj === "symbol") {
|
|
4114
|
+
return undefined;
|
|
4115
|
+
}
|
|
4116
|
+
if (obj === null || typeof obj !== "object") {
|
|
4117
|
+
return JSON.stringify(obj);
|
|
4118
|
+
}
|
|
4119
|
+
if (Array.isArray(obj)) {
|
|
4120
|
+
return JSON.stringify(obj.map((item) => {
|
|
4121
|
+
const val = stableStringify(item);
|
|
4122
|
+
return val === undefined ? null : JSON.parse(val);
|
|
4123
|
+
}));
|
|
4124
|
+
}
|
|
4125
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
4126
|
+
const sortedObj = {};
|
|
4127
|
+
for (const key of sortedKeys) {
|
|
4128
|
+
const value = obj[key];
|
|
4129
|
+
const str = stableStringify(value);
|
|
4130
|
+
// Skip undefined or non-serializable values
|
|
4131
|
+
if (str === undefined)
|
|
4132
|
+
continue;
|
|
4133
|
+
sortedObj[key] = JSON.parse(str);
|
|
4134
|
+
}
|
|
4135
|
+
return JSON.stringify(sortedObj);
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Computes the SHA-256 hash of a JSON-serializable object.
|
|
4139
|
+
* Returns the hash as a hexadecimal string.
|
|
3449
4140
|
*
|
|
3450
|
-
*
|
|
3451
|
-
*
|
|
4141
|
+
* @param content - The content to hash (will be JSON serialized)
|
|
4142
|
+
* @returns The SHA-256 hash of the content
|
|
4143
|
+
* @throws {ValidationError} If content is not serializable (e.g. undefined, function, symbol)
|
|
4144
|
+
*/
|
|
4145
|
+
async function sha256Hash(content) {
|
|
4146
|
+
// Use stable stringification to ensure deterministic output
|
|
4147
|
+
const jsonString = stableStringify(content);
|
|
4148
|
+
if (jsonString === undefined) {
|
|
4149
|
+
throw new ValidationError(`Content illegal: not serializable (type: ${typeof content})`);
|
|
4150
|
+
}
|
|
4151
|
+
const msgBuffer = new TextEncoder().encode(jsonString);
|
|
4152
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
4153
|
+
// Browser / Modern Node.js
|
|
4154
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
|
|
4155
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
4156
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4157
|
+
}
|
|
4158
|
+
else {
|
|
4159
|
+
// Fallback for older environments or specific setups if global crypto isn't available
|
|
4160
|
+
try {
|
|
4161
|
+
// Dynamic import to avoid breaking browser builds if bundler doesn't handle it
|
|
4162
|
+
const { createHash } = await import('node:crypto');
|
|
4163
|
+
const hash = createHash("sha256").update(jsonString).digest("hex");
|
|
4164
|
+
return hash;
|
|
4165
|
+
}
|
|
4166
|
+
catch (e) {
|
|
4167
|
+
throw new NetworkError("SHA-256 hashing not supported in this environment", e);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
/**
|
|
4173
|
+
* Lexicon Development Utilities - AT-URI and StrongRef Helpers
|
|
4174
|
+
*
|
|
4175
|
+
* This module provides utilities for working with AT Protocol URIs and strongRefs
|
|
4176
|
+
* when building custom lexicons. These tools help developers create type-safe
|
|
4177
|
+
* references between records.
|
|
4178
|
+
*
|
|
4179
|
+
* @packageDocumentation
|
|
4180
|
+
*/
|
|
4181
|
+
/**
|
|
4182
|
+
* Regular expression for parsing AT-URIs.
|
|
4183
|
+
*
|
|
4184
|
+
* AT-URIs follow the format: `at://{did}/{collection}/{rkey}`
|
|
4185
|
+
*
|
|
4186
|
+
* Capture groups:
|
|
4187
|
+
* - [1] did - The DID of the repository owner (e.g., "did:plc:abc123")
|
|
4188
|
+
* - [2] collection - The NSID of the record type (e.g., "org.hypercerts.claim.activity")
|
|
4189
|
+
* - [3] rkey - The record key (e.g., "3km2vj4kfqp2a")
|
|
4190
|
+
*
|
|
4191
|
+
* @example Direct regex usage
|
|
4192
|
+
* ```typescript
|
|
4193
|
+
* const match = AT_URI_REGEX.exec("at://did:plc:abc/org.hypercerts.claim.activity/xyz");
|
|
4194
|
+
* if (match) {
|
|
4195
|
+
* const [, did, collection, rkey] = match;
|
|
4196
|
+
* }
|
|
4197
|
+
* ```
|
|
4198
|
+
*
|
|
4199
|
+
* @example Validation
|
|
4200
|
+
* ```typescript
|
|
4201
|
+
* if (AT_URI_REGEX.test(userInput)) {
|
|
4202
|
+
* // Valid AT-URI format
|
|
4203
|
+
* }
|
|
4204
|
+
* ```
|
|
3452
4205
|
*
|
|
3453
4206
|
* @remarks
|
|
3454
|
-
*
|
|
4207
|
+
* For most use cases, prefer using {@link parseAtUri} which provides
|
|
4208
|
+
* better error messages and returns a typed object.
|
|
4209
|
+
*/
|
|
4210
|
+
const AT_URI_REGEX = /^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/;
|
|
4211
|
+
/**
|
|
4212
|
+
* Parse an AT-URI into its component parts.
|
|
4213
|
+
*
|
|
4214
|
+
* Extracts the DID, collection NSID, and record key from an AT-URI string.
|
|
4215
|
+
* AT-URIs follow the format: `at://{did}/{collection}/{rkey}`
|
|
4216
|
+
*
|
|
4217
|
+
* @param uri - The AT-URI to parse
|
|
4218
|
+
* @returns The components of the URI
|
|
4219
|
+
* @throws {Error} If the URI format is invalid
|
|
3455
4220
|
*
|
|
4221
|
+
* @example
|
|
3456
4222
|
* ```typescript
|
|
3457
|
-
*
|
|
3458
|
-
*
|
|
3459
|
-
*
|
|
3460
|
-
*
|
|
3461
|
-
*
|
|
4223
|
+
* const components = parseAtUri("at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a");
|
|
4224
|
+
* console.log(components);
|
|
4225
|
+
* // {
|
|
4226
|
+
* // did: "did:plc:abc123",
|
|
4227
|
+
* // collection: "org.hypercerts.claim.activity",
|
|
4228
|
+
* // rkey: "3km2vj4kfqp2a"
|
|
4229
|
+
* // }
|
|
3462
4230
|
* ```
|
|
4231
|
+
*/
|
|
4232
|
+
function parseAtUri(uri) {
|
|
4233
|
+
if (!uri.startsWith("at://")) {
|
|
4234
|
+
throw new Error(`Invalid AT-URI format: must start with "at://", got "${uri}"`);
|
|
4235
|
+
}
|
|
4236
|
+
const withoutProtocol = uri.slice(5); // Remove "at://"
|
|
4237
|
+
const parts = withoutProtocol.split("/");
|
|
4238
|
+
if (parts.length !== 3) {
|
|
4239
|
+
throw new Error(`Invalid AT-URI format: expected "at://{did}/{collection}/{rkey}", got "${uri}"`);
|
|
4240
|
+
}
|
|
4241
|
+
const [did, collection, rkey] = parts;
|
|
4242
|
+
if (!did || !collection || !rkey) {
|
|
4243
|
+
throw new Error(`Invalid AT-URI format: all components must be non-empty, got "${uri}"`);
|
|
4244
|
+
}
|
|
4245
|
+
return { did, collection, rkey };
|
|
4246
|
+
}
|
|
4247
|
+
/**
|
|
4248
|
+
* Build an AT-URI from its component parts.
|
|
3463
4249
|
*
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
3466
|
-
* - {@link HYPERCERT_LEXICONS} - Array of all hypercert lexicon documents
|
|
3467
|
-
* - {@link HYPERCERT_COLLECTIONS} - Constants for collection NSIDs
|
|
4250
|
+
* Constructs a valid AT-URI string from a DID, collection NSID, and record key.
|
|
4251
|
+
* The resulting URI follows the format: `at://{did}/{collection}/{rkey}`
|
|
3468
4252
|
*
|
|
3469
|
-
* @
|
|
4253
|
+
* @param did - The repository owner's DID
|
|
4254
|
+
* @param collection - The collection NSID (lexicon identifier)
|
|
4255
|
+
* @param rkey - The record key (TID or custom string)
|
|
4256
|
+
* @returns The complete AT-URI
|
|
4257
|
+
*
|
|
4258
|
+
* @example
|
|
3470
4259
|
* ```typescript
|
|
3471
|
-
*
|
|
4260
|
+
* const uri = buildAtUri(
|
|
4261
|
+
* "did:plc:abc123",
|
|
4262
|
+
* "org.hypercerts.claim.activity",
|
|
4263
|
+
* "3km2vj4kfqp2a"
|
|
4264
|
+
* );
|
|
4265
|
+
* console.log(uri); // "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a"
|
|
4266
|
+
* ```
|
|
4267
|
+
*/
|
|
4268
|
+
function buildAtUri(did, collection, rkey) {
|
|
4269
|
+
if (!did || !collection || !rkey) {
|
|
4270
|
+
throw new Error("All AT-URI components (did, collection, rkey) must be non-empty");
|
|
4271
|
+
}
|
|
4272
|
+
return `at://${did}/${collection}/${rkey}`;
|
|
4273
|
+
}
|
|
4274
|
+
/**
|
|
4275
|
+
* Extract the record key (TID or custom key) from an AT-URI.
|
|
3472
4276
|
*
|
|
3473
|
-
*
|
|
3474
|
-
*
|
|
3475
|
-
* collection: HYPERCERT_COLLECTIONS.RECORD,
|
|
3476
|
-
* });
|
|
4277
|
+
* Returns the last component of the AT-URI, which is the record key.
|
|
4278
|
+
* This is equivalent to `parseAtUri(uri).rkey` but more efficient.
|
|
3477
4279
|
*
|
|
3478
|
-
*
|
|
3479
|
-
*
|
|
3480
|
-
*
|
|
3481
|
-
*
|
|
4280
|
+
* @param uri - The AT-URI to extract from
|
|
4281
|
+
* @returns The record key (TID or custom string)
|
|
4282
|
+
* @throws {Error} If the URI format is invalid
|
|
4283
|
+
*
|
|
4284
|
+
* @example
|
|
4285
|
+
* ```typescript
|
|
4286
|
+
* const rkey = extractRkeyFromUri("at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a");
|
|
4287
|
+
* console.log(rkey); // "3km2vj4kfqp2a"
|
|
3482
4288
|
* ```
|
|
4289
|
+
*/
|
|
4290
|
+
function extractRkeyFromUri(uri) {
|
|
4291
|
+
const { rkey } = parseAtUri(uri);
|
|
4292
|
+
return rkey;
|
|
4293
|
+
}
|
|
4294
|
+
/**
|
|
4295
|
+
* Check if a string is a valid AT-URI format.
|
|
3483
4296
|
*
|
|
3484
|
-
*
|
|
4297
|
+
* Validates that the string follows the AT-URI format without throwing errors.
|
|
4298
|
+
* This is useful for input validation before parsing.
|
|
4299
|
+
*
|
|
4300
|
+
* @param uri - The string to validate
|
|
4301
|
+
* @returns True if the string is a valid AT-URI, false otherwise
|
|
4302
|
+
*
|
|
4303
|
+
* @example
|
|
3485
4304
|
* ```typescript
|
|
3486
|
-
*
|
|
4305
|
+
* if (isValidAtUri(userInput)) {
|
|
4306
|
+
* const components = parseAtUri(userInput);
|
|
4307
|
+
* // ... use components
|
|
4308
|
+
* } else {
|
|
4309
|
+
* console.error("Invalid AT-URI");
|
|
4310
|
+
* }
|
|
4311
|
+
* ```
|
|
4312
|
+
*/
|
|
4313
|
+
function isValidAtUri(uri) {
|
|
4314
|
+
try {
|
|
4315
|
+
parseAtUri(uri);
|
|
4316
|
+
return true;
|
|
4317
|
+
}
|
|
4318
|
+
catch {
|
|
4319
|
+
return false;
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
/**
|
|
4323
|
+
* Create a strongRef from a URI and CID.
|
|
3487
4324
|
*
|
|
3488
|
-
*
|
|
4325
|
+
* StrongRefs are the canonical way to reference specific versions of records
|
|
4326
|
+
* in AT Protocol. They combine an AT-URI (which identifies the record) with
|
|
4327
|
+
* a CID (which identifies the specific version).
|
|
4328
|
+
*
|
|
4329
|
+
* @param uri - The AT-URI of the record
|
|
4330
|
+
* @param cid - The CID (Content Identifier) of the record version
|
|
4331
|
+
* @returns A strongRef object
|
|
4332
|
+
*
|
|
4333
|
+
* @example
|
|
4334
|
+
* ```typescript
|
|
4335
|
+
* const ref = createStrongRef(
|
|
4336
|
+
* "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a",
|
|
4337
|
+
* "bafyreiabc123..."
|
|
4338
|
+
* );
|
|
4339
|
+
* console.log(ref);
|
|
4340
|
+
* // {
|
|
4341
|
+
* // uri: "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a",
|
|
4342
|
+
* // cid: "bafyreiabc123..."
|
|
4343
|
+
* // }
|
|
4344
|
+
* ```
|
|
4345
|
+
*/
|
|
4346
|
+
function createStrongRef(uri, cid) {
|
|
4347
|
+
if (!uri || !cid) {
|
|
4348
|
+
throw new Error("Both uri and cid are required to create a strongRef");
|
|
4349
|
+
}
|
|
4350
|
+
return { uri, cid };
|
|
4351
|
+
}
|
|
4352
|
+
/**
|
|
4353
|
+
* Create a strongRef from a CreateResult or UpdateResult.
|
|
4354
|
+
*
|
|
4355
|
+
* This is a convenience function that extracts the URI and CID from
|
|
4356
|
+
* the result of a record creation or update operation.
|
|
4357
|
+
*
|
|
4358
|
+
* @param result - The result from creating or updating a record
|
|
4359
|
+
* @returns A strongRef object
|
|
3489
4360
|
*
|
|
3490
|
-
*
|
|
3491
|
-
*
|
|
3492
|
-
*
|
|
3493
|
-
*
|
|
3494
|
-
*
|
|
4361
|
+
* @example
|
|
4362
|
+
* ```typescript
|
|
4363
|
+
* const hypercert = await repo.hypercerts.create({
|
|
4364
|
+
* title: "Climate Research",
|
|
4365
|
+
* // ... other params
|
|
3495
4366
|
* });
|
|
3496
4367
|
*
|
|
3497
|
-
*
|
|
3498
|
-
*
|
|
3499
|
-
* if (!result.valid) {
|
|
3500
|
-
* console.error(result.error);
|
|
3501
|
-
* }
|
|
4368
|
+
* const ref = createStrongRefFromResult(hypercert);
|
|
4369
|
+
* // Now use ref in another record to reference this hypercert
|
|
3502
4370
|
* ```
|
|
3503
|
-
*
|
|
3504
|
-
* @packageDocumentation
|
|
3505
|
-
*/
|
|
3506
|
-
/**
|
|
3507
|
-
* All hypercert-related lexicons for registration with AT Protocol Agent.
|
|
3508
|
-
* This array contains all lexicon documents from the published package.
|
|
3509
4371
|
*/
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
lexicon.STRONG_REF_LEXICON_JSON,
|
|
3514
|
-
lexicon.HYPERCERTS_DEFS_LEXICON_JSON,
|
|
3515
|
-
lexicon.ACTIVITY_LEXICON_JSON,
|
|
3516
|
-
lexicon.COLLECTION_LEXICON_JSON,
|
|
3517
|
-
lexicon.CONTRIBUTION_DETAILS_LEXICON_JSON,
|
|
3518
|
-
lexicon.CONTRIBUTOR_INFORMATION_LEXICON_JSON,
|
|
3519
|
-
lexicon.EVALUATION_LEXICON_JSON,
|
|
3520
|
-
lexicon.ATTACHMENT_LEXICON_JSON,
|
|
3521
|
-
lexicon.MEASUREMENT_LEXICON_JSON,
|
|
3522
|
-
lexicon.RIGHTS_LEXICON_JSON,
|
|
3523
|
-
lexicon.BADGE_AWARD_LEXICON_JSON,
|
|
3524
|
-
lexicon.BADGE_DEFINITION_LEXICON_JSON,
|
|
3525
|
-
lexicon.BADGE_RESPONSE_LEXICON_JSON,
|
|
3526
|
-
lexicon.FUNDING_RECEIPT_LEXICON_JSON,
|
|
3527
|
-
lexicon.WORK_SCOPE_TAG_LEXICON_JSON,
|
|
3528
|
-
];
|
|
4372
|
+
function createStrongRefFromResult(result) {
|
|
4373
|
+
return createStrongRef(result.uri, result.cid);
|
|
4374
|
+
}
|
|
3529
4375
|
/**
|
|
3530
|
-
*
|
|
4376
|
+
* Validate that an object is a valid strongRef.
|
|
3531
4377
|
*
|
|
3532
|
-
*
|
|
3533
|
-
*
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
*/
|
|
3547
|
-
LOCATION: lexicon.LOCATION_NSID,
|
|
3548
|
-
/**
|
|
3549
|
-
* Contribution details record collection.
|
|
3550
|
-
* For storing details about a specific contribution (role, description, timeframe).
|
|
3551
|
-
*/
|
|
3552
|
-
CONTRIBUTION_DETAILS: lexicon.CONTRIBUTION_DETAILS_NSID,
|
|
3553
|
-
/**
|
|
3554
|
-
* Contributor information record collection.
|
|
3555
|
-
* For storing contributor profile information (identifier, displayName, image).
|
|
3556
|
-
*/
|
|
3557
|
-
CONTRIBUTOR_INFORMATION: lexicon.CONTRIBUTOR_INFORMATION_NSID,
|
|
3558
|
-
/**
|
|
3559
|
-
* Measurement record collection.
|
|
3560
|
-
*/
|
|
3561
|
-
MEASUREMENT: lexicon.MEASUREMENT_NSID,
|
|
3562
|
-
/**
|
|
3563
|
-
* Evaluation record collection.
|
|
3564
|
-
*/
|
|
3565
|
-
EVALUATION: lexicon.EVALUATION_NSID,
|
|
3566
|
-
/**
|
|
3567
|
-
* Attachment record collection.
|
|
3568
|
-
*/
|
|
3569
|
-
ATTACHMENT: lexicon.ATTACHMENT_NSID,
|
|
3570
|
-
/**
|
|
3571
|
-
* Collection record collection (groups of hypercerts).
|
|
3572
|
-
* Projects are now collections with type='project'.
|
|
3573
|
-
*/
|
|
3574
|
-
COLLECTION: lexicon.COLLECTION_NSID,
|
|
3575
|
-
/**
|
|
3576
|
-
* Badge award record collection.
|
|
3577
|
-
*/
|
|
3578
|
-
BADGE_AWARD: lexicon.BADGE_AWARD_NSID,
|
|
3579
|
-
/**
|
|
3580
|
-
* Badge definition record collection.
|
|
3581
|
-
*/
|
|
3582
|
-
BADGE_DEFINITION: lexicon.BADGE_DEFINITION_NSID,
|
|
3583
|
-
/**
|
|
3584
|
-
* Badge response record collection.
|
|
3585
|
-
*/
|
|
3586
|
-
BADGE_RESPONSE: lexicon.BADGE_RESPONSE_NSID,
|
|
3587
|
-
/**
|
|
3588
|
-
* Funding receipt record collection.
|
|
3589
|
-
*/
|
|
3590
|
-
FUNDING_RECEIPT: lexicon.FUNDING_RECEIPT_NSID,
|
|
3591
|
-
/**
|
|
3592
|
-
* Work scope tag record collection.
|
|
3593
|
-
* For defining reusable work scope atoms.
|
|
3594
|
-
*/
|
|
3595
|
-
WORK_SCOPE_TAG: lexicon.WORK_SCOPE_TAG_NSID,
|
|
3596
|
-
};
|
|
3597
|
-
|
|
3598
|
-
/**
|
|
3599
|
-
* Crypto utilities for the SDK.
|
|
3600
|
-
*/
|
|
3601
|
-
/**
|
|
3602
|
-
* Deterministically stringifies an object by sorting keys recursively.
|
|
3603
|
-
* Handles deeply nested objects and null values correctly.
|
|
4378
|
+
* Checks that the object has the required `uri` and `cid` properties
|
|
4379
|
+
* and that they are non-empty strings.
|
|
4380
|
+
*
|
|
4381
|
+
* @param ref - The object to validate
|
|
4382
|
+
* @returns True if the object is a valid strongRef, false otherwise
|
|
4383
|
+
*
|
|
4384
|
+
* @example
|
|
4385
|
+
* ```typescript
|
|
4386
|
+
* const maybeRef = { uri: "at://...", cid: "bafyrei..." };
|
|
4387
|
+
* if (validateStrongRef(maybeRef)) {
|
|
4388
|
+
* // Safe to use as strongRef
|
|
4389
|
+
* record.subject = maybeRef;
|
|
4390
|
+
* }
|
|
4391
|
+
* ```
|
|
3604
4392
|
*/
|
|
3605
|
-
function
|
|
3606
|
-
if (
|
|
3607
|
-
return
|
|
3608
|
-
}
|
|
3609
|
-
if (obj === null || typeof obj !== "object") {
|
|
3610
|
-
return JSON.stringify(obj);
|
|
3611
|
-
}
|
|
3612
|
-
if (Array.isArray(obj)) {
|
|
3613
|
-
return JSON.stringify(obj.map((item) => {
|
|
3614
|
-
const val = stableStringify(item);
|
|
3615
|
-
return val === undefined ? null : JSON.parse(val);
|
|
3616
|
-
}));
|
|
3617
|
-
}
|
|
3618
|
-
const sortedKeys = Object.keys(obj).sort();
|
|
3619
|
-
const sortedObj = {};
|
|
3620
|
-
for (const key of sortedKeys) {
|
|
3621
|
-
const value = obj[key];
|
|
3622
|
-
const str = stableStringify(value);
|
|
3623
|
-
// Skip undefined or non-serializable values
|
|
3624
|
-
if (str === undefined)
|
|
3625
|
-
continue;
|
|
3626
|
-
sortedObj[key] = JSON.parse(str);
|
|
4393
|
+
function validateStrongRef(ref) {
|
|
4394
|
+
if (!ref || typeof ref !== "object") {
|
|
4395
|
+
return false;
|
|
3627
4396
|
}
|
|
3628
|
-
|
|
4397
|
+
const obj = ref;
|
|
4398
|
+
return typeof obj.uri === "string" && obj.uri.length > 0 && typeof obj.cid === "string" && obj.cid.length > 0;
|
|
3629
4399
|
}
|
|
3630
4400
|
/**
|
|
3631
|
-
*
|
|
3632
|
-
* Returns the hash as a hexadecimal string.
|
|
4401
|
+
* Type guard to check if a value is a strongRef.
|
|
3633
4402
|
*
|
|
3634
|
-
*
|
|
3635
|
-
*
|
|
3636
|
-
*
|
|
4403
|
+
* This is an alias for `validateStrongRef` that provides better semantics
|
|
4404
|
+
* for type narrowing in TypeScript.
|
|
4405
|
+
*
|
|
4406
|
+
* @param value - The value to check
|
|
4407
|
+
* @returns True if the value is a strongRef, false otherwise
|
|
4408
|
+
*
|
|
4409
|
+
* @example
|
|
4410
|
+
* ```typescript
|
|
4411
|
+
* function processReference(ref: unknown) {
|
|
4412
|
+
* if (isStrongRef(ref)) {
|
|
4413
|
+
* // TypeScript knows ref is StrongRef here
|
|
4414
|
+
* console.log(ref.uri);
|
|
4415
|
+
* }
|
|
4416
|
+
* }
|
|
4417
|
+
* ```
|
|
3637
4418
|
*/
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
const jsonString = stableStringify(content);
|
|
3641
|
-
if (jsonString === undefined) {
|
|
3642
|
-
throw new ValidationError(`Content illegal: not serializable (type: ${typeof content})`);
|
|
3643
|
-
}
|
|
3644
|
-
const msgBuffer = new TextEncoder().encode(jsonString);
|
|
3645
|
-
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
3646
|
-
// Browser / Modern Node.js
|
|
3647
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
|
|
3648
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
3649
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3650
|
-
}
|
|
3651
|
-
else {
|
|
3652
|
-
// Fallback for older environments or specific setups if global crypto isn't available
|
|
3653
|
-
try {
|
|
3654
|
-
// Dynamic import to avoid breaking browser builds if bundler doesn't handle it
|
|
3655
|
-
const { createHash } = await import('node:crypto');
|
|
3656
|
-
const hash = createHash("sha256").update(jsonString).digest("hex");
|
|
3657
|
-
return hash;
|
|
3658
|
-
}
|
|
3659
|
-
catch (e) {
|
|
3660
|
-
throw new NetworkError("SHA-256 hashing not supported in this environment", e);
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
4419
|
+
function isStrongRef(value) {
|
|
4420
|
+
return validateStrongRef(value);
|
|
3663
4421
|
}
|
|
3664
4422
|
|
|
3665
4423
|
/**
|
|
@@ -3720,21 +4478,98 @@ async function sha256Hash(content) {
|
|
|
3720
4478
|
*/
|
|
3721
4479
|
class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
3722
4480
|
/**
|
|
3723
|
-
* Creates a new HypercertOperationsImpl.
|
|
3724
|
-
*
|
|
3725
|
-
* @param agent - AT Protocol Agent for making API calls
|
|
3726
|
-
* @param repoDid - DID of the repository to operate on
|
|
3727
|
-
* @param blobs - Blob operations for uploading images and files
|
|
3728
|
-
* @param logger - Optional logger for debugging
|
|
4481
|
+
* Creates a new HypercertOperationsImpl.
|
|
4482
|
+
*
|
|
4483
|
+
* @param agent - AT Protocol Agent for making API calls
|
|
4484
|
+
* @param repoDid - DID of the repository to operate on
|
|
4485
|
+
* @param blobs - Blob operations for uploading images and files
|
|
4486
|
+
* @param logger - Optional logger for debugging
|
|
4487
|
+
*
|
|
4488
|
+
* @internal
|
|
4489
|
+
*/
|
|
4490
|
+
constructor(agent, repoDid, blobs, logger) {
|
|
4491
|
+
super();
|
|
4492
|
+
this.agent = agent;
|
|
4493
|
+
this.repoDid = repoDid;
|
|
4494
|
+
this.blobs = blobs;
|
|
4495
|
+
this.logger = logger;
|
|
4496
|
+
}
|
|
4497
|
+
/**
|
|
4498
|
+
* Parses an AT-URI and throws ValidationError if invalid.
|
|
4499
|
+
*
|
|
4500
|
+
* This wrapper converts the generic Error from parseAtUri() to a ValidationError
|
|
4501
|
+
* for consistent error handling throughout the SDK.
|
|
4502
|
+
*
|
|
4503
|
+
* @param uri - AT-URI to parse
|
|
4504
|
+
* @returns Parsed URI components
|
|
4505
|
+
* @throws {@link ValidationError} if URI format is invalid
|
|
4506
|
+
* @internal
|
|
4507
|
+
*/
|
|
4508
|
+
parseUri(uri) {
|
|
4509
|
+
try {
|
|
4510
|
+
return parseAtUri(uri);
|
|
4511
|
+
}
|
|
4512
|
+
catch (error) {
|
|
4513
|
+
throw new ValidationError(error instanceof Error ? error.message : `Invalid URI format: ${uri}`);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
/**
|
|
4517
|
+
* Fetches any record by AT-URI with generic typing.
|
|
4518
|
+
*
|
|
4519
|
+
* Returns the record along with parsed URI components needed for updates.
|
|
4520
|
+
* Unlike the public `get()` method which is typed for HypercertClaim,
|
|
4521
|
+
* this method can fetch any record type.
|
|
4522
|
+
*
|
|
4523
|
+
* @typeParam T - The expected type of the record
|
|
4524
|
+
* @param uri - AT-URI of the record to fetch
|
|
4525
|
+
* @returns Record data with parsed URI components
|
|
4526
|
+
* @throws {@link ValidationError} if URI format is invalid
|
|
4527
|
+
* @throws {@link NetworkError} if record cannot be fetched
|
|
4528
|
+
* @internal
|
|
4529
|
+
*/
|
|
4530
|
+
async fetchRecord(uri) {
|
|
4531
|
+
const { did, collection, rkey } = this.parseUri(uri);
|
|
4532
|
+
const result = await this.agent.com.atproto.repo.getRecord({
|
|
4533
|
+
repo: did,
|
|
4534
|
+
collection,
|
|
4535
|
+
rkey,
|
|
4536
|
+
});
|
|
4537
|
+
if (!result.success) {
|
|
4538
|
+
throw new NetworkError(`Failed to fetch record: ${uri}`);
|
|
4539
|
+
}
|
|
4540
|
+
const cid = result.data.cid;
|
|
4541
|
+
if (!cid) {
|
|
4542
|
+
throw new NetworkError(`Record at ${uri} returned no CID`);
|
|
4543
|
+
}
|
|
4544
|
+
return {
|
|
4545
|
+
uri: result.data.uri,
|
|
4546
|
+
cid,
|
|
4547
|
+
record: result.data.value,
|
|
4548
|
+
collection,
|
|
4549
|
+
rkey,
|
|
4550
|
+
};
|
|
4551
|
+
}
|
|
4552
|
+
/**
|
|
4553
|
+
* Updates a record in the repository.
|
|
3729
4554
|
*
|
|
4555
|
+
* @param collection - NSID of the collection
|
|
4556
|
+
* @param rkey - Record key
|
|
4557
|
+
* @param record - Updated record data
|
|
4558
|
+
* @returns Update result with URI and CID
|
|
4559
|
+
* @throws {@link NetworkError} if update fails
|
|
3730
4560
|
* @internal
|
|
3731
4561
|
*/
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
4562
|
+
async saveRecord(collection, rkey, record) {
|
|
4563
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
4564
|
+
repo: this.repoDid,
|
|
4565
|
+
collection,
|
|
4566
|
+
rkey,
|
|
4567
|
+
record,
|
|
4568
|
+
});
|
|
4569
|
+
if (!result.success) {
|
|
4570
|
+
throw new NetworkError(`Failed to save record: ${collection}/${rkey}`);
|
|
4571
|
+
}
|
|
4572
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
3738
4573
|
}
|
|
3739
4574
|
/**
|
|
3740
4575
|
* Emits a progress event to the optional progress handler.
|
|
@@ -3753,16 +4588,6 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3753
4588
|
}
|
|
3754
4589
|
}
|
|
3755
4590
|
}
|
|
3756
|
-
/**
|
|
3757
|
-
* Converts a blob upload result to JsonBlobRef format.
|
|
3758
|
-
*
|
|
3759
|
-
* @param uploadResult - Result from BlobOperations.upload()
|
|
3760
|
-
* @returns JsonBlobRef formatted for records
|
|
3761
|
-
* @internal
|
|
3762
|
-
*/
|
|
3763
|
-
blobToJsonRef(uploadResult) {
|
|
3764
|
-
return uploadResultToBlobRef(uploadResult);
|
|
3765
|
-
}
|
|
3766
4591
|
/**
|
|
3767
4592
|
* Uploads an image blob and returns a blob reference.
|
|
3768
4593
|
*
|
|
@@ -3781,7 +4606,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3781
4606
|
status: "success",
|
|
3782
4607
|
data: { size: image.size },
|
|
3783
4608
|
});
|
|
3784
|
-
return
|
|
4609
|
+
return uploadResult;
|
|
3785
4610
|
}
|
|
3786
4611
|
catch (error) {
|
|
3787
4612
|
this.emitProgress(onProgress, { name: "uploadImage", status: "error", error: error });
|
|
@@ -3860,7 +4685,10 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3860
4685
|
createdAt,
|
|
3861
4686
|
};
|
|
3862
4687
|
if (imageBlobRef) {
|
|
3863
|
-
hypercertRecord.image =
|
|
4688
|
+
hypercertRecord.image = {
|
|
4689
|
+
$type: "org.hypercerts.defs#smallImage",
|
|
4690
|
+
image: imageBlobRef,
|
|
4691
|
+
};
|
|
3864
4692
|
}
|
|
3865
4693
|
// Add locations as embedded StrongRefs if provided
|
|
3866
4694
|
if (locationRefs && locationRefs.length > 0) {
|
|
@@ -3892,6 +4720,11 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3892
4720
|
if (!hypercertValidation.success) {
|
|
3893
4721
|
throw new ValidationError(`Invalid hypercert record: ${hypercertValidation.error?.message}`);
|
|
3894
4722
|
}
|
|
4723
|
+
// if its a blob ref guaranteed to have ref and a .toString method
|
|
4724
|
+
let imageRef;
|
|
4725
|
+
if (imageBlobRef) {
|
|
4726
|
+
imageRef = imageBlobRef.ref.toString();
|
|
4727
|
+
}
|
|
3895
4728
|
// Generate rKey from stable content hash (idempotency)
|
|
3896
4729
|
// Use NORMALIZED values (already resolved StrongRefs and processed data)
|
|
3897
4730
|
// to ensure JSON-serializability and deterministic hashing.
|
|
@@ -3904,15 +4737,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3904
4737
|
workScope: params.workScope,
|
|
3905
4738
|
startDate: params.startDate,
|
|
3906
4739
|
endDate: params.endDate,
|
|
3907
|
-
|
|
3908
|
-
// JsonBlobRef can have ref.$link (upload result) or cid (existing record)
|
|
3909
|
-
imageRef: imageBlobRef
|
|
3910
|
-
? "ref" in imageBlobRef && imageBlobRef.ref
|
|
3911
|
-
? imageBlobRef.ref.$link
|
|
3912
|
-
: "cid" in imageBlobRef
|
|
3913
|
-
? imageBlobRef.cid
|
|
3914
|
-
: undefined
|
|
3915
|
-
: undefined,
|
|
4740
|
+
imageRef,
|
|
3916
4741
|
// Rights: canonical object with only known fields
|
|
3917
4742
|
rights: {
|
|
3918
4743
|
name: params.rights.name,
|
|
@@ -3971,41 +4796,6 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
3971
4796
|
throw error;
|
|
3972
4797
|
}
|
|
3973
4798
|
}
|
|
3974
|
-
/**
|
|
3975
|
-
* Creates contribution records with progress tracking.
|
|
3976
|
-
*
|
|
3977
|
-
* @param hypercertUri - URI of the hypercert
|
|
3978
|
-
* @param contributions - Array of contribution data
|
|
3979
|
-
* @param onProgress - Optional progress callback
|
|
3980
|
-
* @returns Promise resolving to array of contribution URIs
|
|
3981
|
-
* @internal
|
|
3982
|
-
*/
|
|
3983
|
-
async createContributionsWithProgress(hypercertUri, contributions, onProgress) {
|
|
3984
|
-
this.emitProgress(onProgress, { name: "createContributions", status: "start" });
|
|
3985
|
-
try {
|
|
3986
|
-
const contributionUris = [];
|
|
3987
|
-
for (const contrib of contributions) {
|
|
3988
|
-
const contribResult = await this.addContribution({
|
|
3989
|
-
hypercertUri,
|
|
3990
|
-
contributors: contrib.contributors.filter((c) => typeof c === "string"),
|
|
3991
|
-
role: contrib.role,
|
|
3992
|
-
description: contrib.description,
|
|
3993
|
-
});
|
|
3994
|
-
contributionUris.push(contribResult.uri);
|
|
3995
|
-
}
|
|
3996
|
-
this.emitProgress(onProgress, {
|
|
3997
|
-
name: "createContributions",
|
|
3998
|
-
status: "success",
|
|
3999
|
-
data: { count: contributionUris.length },
|
|
4000
|
-
});
|
|
4001
|
-
return contributionUris;
|
|
4002
|
-
}
|
|
4003
|
-
catch (error) {
|
|
4004
|
-
this.emitProgress(onProgress, { name: "createContributions", status: "error", error: error });
|
|
4005
|
-
this.logger?.warn(`Failed to create contributions: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
4006
|
-
throw error;
|
|
4007
|
-
}
|
|
4008
|
-
}
|
|
4009
4799
|
/**
|
|
4010
4800
|
* Creates attachment records and returns their URIs.
|
|
4011
4801
|
*
|
|
@@ -4194,19 +4984,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4194
4984
|
*/
|
|
4195
4985
|
async update(params) {
|
|
4196
4986
|
try {
|
|
4197
|
-
const
|
|
4198
|
-
if (!uriMatch) {
|
|
4199
|
-
throw new ValidationError(`Invalid URI format: ${params.uri}`);
|
|
4200
|
-
}
|
|
4201
|
-
const [, , collection, rkey] = uriMatch;
|
|
4202
|
-
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4203
|
-
repo: this.repoDid,
|
|
4204
|
-
collection,
|
|
4205
|
-
rkey,
|
|
4206
|
-
});
|
|
4207
|
-
// The existing record comes from ATProto, use it directly
|
|
4208
|
-
// TypeScript ensures type safety through the HypercertClaim interface
|
|
4209
|
-
const existingRecord = existing.data.value;
|
|
4987
|
+
const { record: existingRecord, collection, rkey } = await this.fetchRecord(params.uri);
|
|
4210
4988
|
const recordForUpdate = {
|
|
4211
4989
|
...existingRecord,
|
|
4212
4990
|
...params.updates,
|
|
@@ -4221,7 +4999,10 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4221
4999
|
}
|
|
4222
5000
|
else {
|
|
4223
5001
|
const uploadResult = await this.blobs.upload(params.image);
|
|
4224
|
-
recordForUpdate.image =
|
|
5002
|
+
recordForUpdate.image = {
|
|
5003
|
+
$type: "org.hypercerts.defs#smallImage",
|
|
5004
|
+
image: uploadResult,
|
|
5005
|
+
};
|
|
4225
5006
|
}
|
|
4226
5007
|
}
|
|
4227
5008
|
else if (existingRecord.image) {
|
|
@@ -4232,17 +5013,9 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4232
5013
|
if (!validation.success) {
|
|
4233
5014
|
throw new ValidationError(`Invalid hypercert record: ${validation.error?.message}`);
|
|
4234
5015
|
}
|
|
4235
|
-
const result = await this.
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
rkey,
|
|
4239
|
-
record: recordForUpdate,
|
|
4240
|
-
});
|
|
4241
|
-
if (!result.success) {
|
|
4242
|
-
throw new NetworkError("Failed to update hypercert");
|
|
4243
|
-
}
|
|
4244
|
-
this.emit("recordUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
4245
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
5016
|
+
const result = await this.saveRecord(collection, rkey, recordForUpdate);
|
|
5017
|
+
this.emit("recordUpdated", { uri: result.uri, cid: result.cid });
|
|
5018
|
+
return result;
|
|
4246
5019
|
}
|
|
4247
5020
|
catch (error) {
|
|
4248
5021
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -4266,24 +5039,8 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4266
5039
|
*/
|
|
4267
5040
|
async get(uri) {
|
|
4268
5041
|
try {
|
|
4269
|
-
const
|
|
4270
|
-
|
|
4271
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4272
|
-
}
|
|
4273
|
-
const [, , collection, rkey] = uriMatch;
|
|
4274
|
-
const result = await this.agent.com.atproto.repo.getRecord({
|
|
4275
|
-
repo: this.repoDid,
|
|
4276
|
-
collection,
|
|
4277
|
-
rkey,
|
|
4278
|
-
});
|
|
4279
|
-
if (!result.success) {
|
|
4280
|
-
throw new NetworkError("Failed to get hypercert");
|
|
4281
|
-
}
|
|
4282
|
-
return {
|
|
4283
|
-
uri: result.data.uri,
|
|
4284
|
-
cid: result.data.cid ?? "",
|
|
4285
|
-
record: result.data.value,
|
|
4286
|
-
};
|
|
5042
|
+
const { uri: resultUri, cid, record } = await this.fetchRecord(uri);
|
|
5043
|
+
return { uri: resultUri, cid, record };
|
|
4287
5044
|
}
|
|
4288
5045
|
catch (error) {
|
|
4289
5046
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -4353,11 +5110,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4353
5110
|
*/
|
|
4354
5111
|
async delete(uri) {
|
|
4355
5112
|
try {
|
|
4356
|
-
const
|
|
4357
|
-
if (!uriMatch) {
|
|
4358
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4359
|
-
}
|
|
4360
|
-
const [, , collection, rkey] = uriMatch;
|
|
5113
|
+
const { collection, rkey } = this.parseUri(uri);
|
|
4361
5114
|
const result = await this.agent.com.atproto.repo.deleteRecord({
|
|
4362
5115
|
repo: this.repoDid,
|
|
4363
5116
|
collection,
|
|
@@ -4521,23 +5274,13 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4521
5274
|
if (this.isLocationObject(location)) {
|
|
4522
5275
|
return this.createLocationRecord(location);
|
|
4523
5276
|
}
|
|
4524
|
-
// Otherwise it's
|
|
5277
|
+
// Otherwise it's RefUri, resolve to StrongRef
|
|
4525
5278
|
return this.resolveToStrongRef(location);
|
|
4526
5279
|
}
|
|
4527
5280
|
async resolveStrongRefFromUri(uri) {
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
}
|
|
4532
|
-
const [, repo, collection, rkey] = uriMatch;
|
|
4533
|
-
const record = await this.agent.com.atproto.repo.getRecord({ repo, collection, rkey });
|
|
4534
|
-
if (!record.success) {
|
|
4535
|
-
throw new NetworkError(`Failed to fetch record for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4536
|
-
}
|
|
4537
|
-
if (!record.data.cid) {
|
|
4538
|
-
throw new NetworkError(`Record missing CID for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4539
|
-
}
|
|
4540
|
-
return { $type: "com.atproto.repo.strongRef", uri, cid: record.data.cid };
|
|
5281
|
+
// fetchRecord already validates CID presence and throws NetworkError if absent
|
|
5282
|
+
const fetchResult = await this.fetchRecord(uri);
|
|
5283
|
+
return { $type: "com.atproto.repo.strongRef", uri, cid: fetchResult.cid };
|
|
4541
5284
|
}
|
|
4542
5285
|
/**
|
|
4543
5286
|
* Resolves a string URI or StrongRef to a StrongRef.
|
|
@@ -4586,11 +5329,18 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4586
5329
|
*
|
|
4587
5330
|
* @param contentInput - Single content item or array (URI strings or Blobs)
|
|
4588
5331
|
* @returns Promise resolving to array of URI refs or Blob refs
|
|
5332
|
+
* @throws {@link ValidationError} if a string content item is not a valid URI
|
|
4589
5333
|
* @throws {@link NetworkError} if blob upload fails
|
|
4590
5334
|
* @internal
|
|
4591
5335
|
*/
|
|
4592
5336
|
async resolveAttachmentContent(contentInput) {
|
|
4593
5337
|
const contentArray = Array.isArray(contentInput) ? contentInput : [contentInput];
|
|
5338
|
+
// Validate that all string content items are valid URIs before resolving
|
|
5339
|
+
for (const item of contentArray) {
|
|
5340
|
+
if (typeof item === "string" && !isValidUri(item)) {
|
|
5341
|
+
throw new ValidationError(`Invalid URI: "${item}". Content must be a valid URI with a scheme (e.g., https://example.com)`);
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
4594
5344
|
return await Promise.all(contentArray.map((item) => this.resolveUriOrBlob(item, "application/octet-stream")));
|
|
4595
5345
|
}
|
|
4596
5346
|
/**
|
|
@@ -4766,23 +5516,42 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4766
5516
|
async processContributors(contributions, onProgress) {
|
|
4767
5517
|
if (!contributions || contributions.length === 0)
|
|
4768
5518
|
return undefined;
|
|
4769
|
-
const
|
|
4770
|
-
|
|
4771
|
-
const detailsRef = await this.resolveContributionDetails(contrib.contributionDetails, onProgress);
|
|
4772
|
-
// Resolve each contributor identity
|
|
4773
|
-
const resolvedContributors = await Promise.all(contrib.contributors.map((identity) => this.resolveContributorIdentity(identity, onProgress)));
|
|
4774
|
-
// Expand to one entry per contributor
|
|
4775
|
-
return resolvedContributors.map((identity) => ({
|
|
4776
|
-
contributorIdentity: identity,
|
|
4777
|
-
contributionWeight: contrib.weight,
|
|
4778
|
-
contributionDetails: detailsRef,
|
|
4779
|
-
}));
|
|
4780
|
-
});
|
|
4781
|
-
const nestedContributors = await Promise.all(contributorsPromises);
|
|
5519
|
+
const contributorPromises = contributions.map((contrib) => this.buildContributorEntries(contrib.contributors, contrib.contributionDetails, contrib.weight, onProgress));
|
|
5520
|
+
const nestedContributors = await Promise.all(contributorPromises);
|
|
4782
5521
|
return nestedContributors.flat();
|
|
4783
5522
|
}
|
|
4784
5523
|
/**
|
|
4785
|
-
*
|
|
5524
|
+
* Creates a standalone contributionDetails record.
|
|
5525
|
+
* @internal
|
|
5526
|
+
*/
|
|
5527
|
+
async createContributionDetailsRecord(params) {
|
|
5528
|
+
const createdAt = new Date().toISOString();
|
|
5529
|
+
const { role, contributionDescription, startDate, endDate, ...extraProps } = params;
|
|
5530
|
+
const contributionRecord = {
|
|
5531
|
+
$type: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
|
|
5532
|
+
role,
|
|
5533
|
+
createdAt,
|
|
5534
|
+
contributionDescription,
|
|
5535
|
+
startDate,
|
|
5536
|
+
endDate,
|
|
5537
|
+
...extraProps,
|
|
5538
|
+
};
|
|
5539
|
+
const validation = lexicon.validate(contributionRecord, HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS, "main", false);
|
|
5540
|
+
if (!validation.success) {
|
|
5541
|
+
throw new ValidationError(`Invalid contribution details record: ${validation.error?.message}`);
|
|
5542
|
+
}
|
|
5543
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
5544
|
+
repo: this.repoDid,
|
|
5545
|
+
collection: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
|
|
5546
|
+
record: contributionRecord,
|
|
5547
|
+
});
|
|
5548
|
+
if (!result.success) {
|
|
5549
|
+
throw new NetworkError("Failed to create contribution details");
|
|
5550
|
+
}
|
|
5551
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
5552
|
+
}
|
|
5553
|
+
/**
|
|
5554
|
+
* Resolves ContributionDetailsParams to a RefUri.
|
|
4786
5555
|
* Creates a record if CreateContributionDetailsParams is provided.
|
|
4787
5556
|
* @internal
|
|
4788
5557
|
*/
|
|
@@ -4799,14 +5568,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4799
5568
|
// CreateContributionDetailsParams - auto-create record
|
|
4800
5569
|
try {
|
|
4801
5570
|
this.emitProgress(onProgress, { name: "createContribution", status: "start" });
|
|
4802
|
-
const
|
|
4803
|
-
const result = await this.addContribution({
|
|
4804
|
-
role,
|
|
4805
|
-
description: contributionDescription,
|
|
4806
|
-
startDate,
|
|
4807
|
-
endDate,
|
|
4808
|
-
...extraProps,
|
|
4809
|
-
});
|
|
5571
|
+
const result = await this.createContributionDetailsRecord(details);
|
|
4810
5572
|
this.emitProgress(onProgress, {
|
|
4811
5573
|
name: "createContribution",
|
|
4812
5574
|
status: "success",
|
|
@@ -4826,13 +5588,13 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4826
5588
|
throw new ValidationError("Invalid contributionDetails format");
|
|
4827
5589
|
}
|
|
4828
5590
|
/**
|
|
4829
|
-
* Resolves ContributorIdentityParams to a
|
|
5591
|
+
* Resolves ContributorIdentityParams to a RefUri.
|
|
4830
5592
|
* Creates a contributorInformation record if CreateContributorInformationParams is provided.
|
|
4831
5593
|
* @internal
|
|
4832
5594
|
*/
|
|
4833
5595
|
async resolveContributorIdentity(identity, onProgress) {
|
|
4834
5596
|
if (typeof identity === "string") {
|
|
4835
|
-
// we still store as
|
|
5597
|
+
// we still store as contributorInformation since it can't directly be a string
|
|
4836
5598
|
const result = await this.addContributorInformation({ identifier: identity });
|
|
4837
5599
|
return { uri: result.uri, cid: result.cid, $type: "com.atproto.repo.strongRef" };
|
|
4838
5600
|
}
|
|
@@ -4879,62 +5641,100 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4879
5641
|
throw new ValidationError("Invalid contributorIdentity format");
|
|
4880
5642
|
}
|
|
4881
5643
|
/**
|
|
4882
|
-
*
|
|
5644
|
+
* Builds contributor entries from parameters by resolving identities and details.
|
|
4883
5645
|
*
|
|
4884
|
-
* This
|
|
4885
|
-
*
|
|
5646
|
+
* This helper resolves contributor identities and contribution details,
|
|
5647
|
+
* creating records as needed, then assembles them into the contributor entry
|
|
5648
|
+
* format used in hypercert records.
|
|
5649
|
+
*
|
|
5650
|
+
* @param contributorParams - Array of contributor identity params (DID, StrongRef, or create params)
|
|
5651
|
+
* @param detailsParams - Contribution details (inline role, StrongRef, or create params)
|
|
5652
|
+
* @param weight - Optional contribution weight
|
|
5653
|
+
* @param onProgress - Optional progress callback
|
|
5654
|
+
* @returns Promise resolving to array of contributor entries ready for embedding
|
|
5655
|
+
* @internal
|
|
5656
|
+
* @protected
|
|
5657
|
+
*/
|
|
5658
|
+
async buildContributorEntries(contributorParams, detailsParams, weight, onProgress) {
|
|
5659
|
+
const detailsRef = await this.resolveContributionDetails(detailsParams, onProgress);
|
|
5660
|
+
const resolvedIdentities = await Promise.all(contributorParams.map((identity) => this.resolveContributorIdentity(identity, onProgress)));
|
|
5661
|
+
return resolvedIdentities.map((identity) => ({
|
|
5662
|
+
contributorIdentity: identity,
|
|
5663
|
+
contributionWeight: weight,
|
|
5664
|
+
contributionDetails: detailsRef,
|
|
5665
|
+
}));
|
|
5666
|
+
}
|
|
5667
|
+
/**
|
|
5668
|
+
* Attaches contributor entries to a hypercert by appending to its contributors array.
|
|
5669
|
+
*
|
|
5670
|
+
* Fetches the existing hypercert, merges new contributors with existing ones,
|
|
5671
|
+
* and updates the hypercert record.
|
|
5672
|
+
*
|
|
5673
|
+
* @param hypercertUri - URI of the hypercert to update
|
|
5674
|
+
* @param newContributors - Array of contributor entries to add
|
|
5675
|
+
* @returns Promise resolving to update result with new URI and CID
|
|
5676
|
+
* @throws {@link ValidationError} if URI format is invalid or validation fails
|
|
5677
|
+
* @throws {@link NetworkError} if fetching or updating fails
|
|
5678
|
+
* @internal
|
|
5679
|
+
* @protected
|
|
5680
|
+
*/
|
|
5681
|
+
async attachContributorsToHypercert(hypercertUri, newContributors) {
|
|
5682
|
+
const existing = await this.get(hypercertUri);
|
|
5683
|
+
const existingContributors = existing.record.contributors || [];
|
|
5684
|
+
const updatedContributors = [...existingContributors, ...newContributors];
|
|
5685
|
+
return await this.update({
|
|
5686
|
+
uri: hypercertUri,
|
|
5687
|
+
updates: {
|
|
5688
|
+
contributors: updatedContributors,
|
|
5689
|
+
},
|
|
5690
|
+
});
|
|
5691
|
+
}
|
|
5692
|
+
/**
|
|
5693
|
+
* Adds contributors to an existing hypercert.
|
|
5694
|
+
*
|
|
5695
|
+
* This method creates or references contribution records and updates the hypercert
|
|
5696
|
+
* to include the new contributors in its contributors array.
|
|
4886
5697
|
*
|
|
4887
5698
|
* @param params - Contribution parameters
|
|
4888
|
-
* @
|
|
4889
|
-
* @param params.contributors - Array of contributor DIDs (unused, kept for backward compatibility)
|
|
4890
|
-
* @param params.role - Role of the contributor (e.g., "coordinator", "implementer")
|
|
4891
|
-
* @param params.description - Optional description of the contribution
|
|
4892
|
-
* @returns Promise resolving to contribution details record URI and CID
|
|
5699
|
+
* @returns Promise resolving to updated hypercert URI and CID
|
|
4893
5700
|
* @throws {@link ValidationError} if validation fails
|
|
4894
5701
|
* @throws {@link NetworkError} if the operation fails
|
|
4895
5702
|
*
|
|
4896
|
-
* @
|
|
4897
|
-
*
|
|
4898
|
-
*
|
|
4899
|
-
*
|
|
4900
|
-
*
|
|
5703
|
+
* @example Add multiple contributors with inline role
|
|
5704
|
+
* ```typescript
|
|
5705
|
+
* await repo.hypercerts.addContribution({
|
|
5706
|
+
* hypercertUri: "at://did:plc:abc/org.hypercerts.claim.activity/xyz",
|
|
5707
|
+
* contributors: ["did:plc:user1", "did:plc:user2"],
|
|
5708
|
+
* contributionDetails: "Developer",
|
|
5709
|
+
* weight: "1.0"
|
|
5710
|
+
* });
|
|
5711
|
+
* ```
|
|
4901
5712
|
*
|
|
4902
|
-
* @example
|
|
5713
|
+
* @example Add contributor with detailed contribution record
|
|
4903
5714
|
* ```typescript
|
|
4904
5715
|
* await repo.hypercerts.addContribution({
|
|
4905
|
-
*
|
|
4906
|
-
*
|
|
5716
|
+
* hypercertUri: hypercertUri,
|
|
5717
|
+
* contributors: [{
|
|
5718
|
+
* identifier: "did:plc:coordinator",
|
|
5719
|
+
* displayName: "Alice",
|
|
5720
|
+
* image: avatarBlob
|
|
5721
|
+
* }],
|
|
5722
|
+
* contributionDetails: {
|
|
5723
|
+
* role: "Project Coordinator",
|
|
5724
|
+
* contributionDescription: "Led coordination efforts",
|
|
5725
|
+
* startDate: "2024-01-01",
|
|
5726
|
+
* endDate: "2024-06-30"
|
|
5727
|
+
* },
|
|
5728
|
+
* weight: "2.0"
|
|
4907
5729
|
* });
|
|
4908
5730
|
* ```
|
|
4909
5731
|
*/
|
|
4910
5732
|
async addContribution(params) {
|
|
4911
5733
|
try {
|
|
4912
|
-
const
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
$type: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
|
|
4917
|
-
role,
|
|
4918
|
-
createdAt,
|
|
4919
|
-
contributionDescription: description,
|
|
4920
|
-
startDate,
|
|
4921
|
-
endDate,
|
|
4922
|
-
...extraProps,
|
|
4923
|
-
};
|
|
4924
|
-
const validation = lexicon.validate(contributionRecord, HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS, "main", false);
|
|
4925
|
-
if (!validation.success) {
|
|
4926
|
-
throw new ValidationError(`Invalid contribution details record: ${validation.error?.message}`);
|
|
4927
|
-
}
|
|
4928
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4929
|
-
repo: this.repoDid,
|
|
4930
|
-
collection: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
|
|
4931
|
-
record: contributionRecord,
|
|
4932
|
-
});
|
|
4933
|
-
if (!result.success) {
|
|
4934
|
-
throw new NetworkError("Failed to create contribution details");
|
|
4935
|
-
}
|
|
4936
|
-
this.emit("contributionCreated", { uri: result.data.uri, cid: result.data.cid });
|
|
4937
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
5734
|
+
const newContributors = await this.buildContributorEntries(params.contributors, params.contributionDetails, params.weight, params.onProgress);
|
|
5735
|
+
const result = await this.attachContributorsToHypercert(params.hypercertUri, newContributors);
|
|
5736
|
+
this.emit("contributionCreated", { uri: result.uri, cid: result.cid });
|
|
5737
|
+
return result;
|
|
4938
5738
|
}
|
|
4939
5739
|
catch (error) {
|
|
4940
5740
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -4976,7 +5776,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4976
5776
|
resolvedImage = { $type: "org.hypercerts.defs#uri", uri: image };
|
|
4977
5777
|
}
|
|
4978
5778
|
else {
|
|
4979
|
-
//
|
|
5779
|
+
// BlobRef from upload - wrap in smallImage
|
|
4980
5780
|
resolvedImage = {
|
|
4981
5781
|
$type: "org.hypercerts.defs#smallImage",
|
|
4982
5782
|
image: image,
|
|
@@ -5114,38 +5914,19 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5114
5914
|
*/
|
|
5115
5915
|
async updateMeasurement(uri, updates) {
|
|
5116
5916
|
try {
|
|
5117
|
-
const
|
|
5118
|
-
if (!uriMatch) {
|
|
5119
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5120
|
-
}
|
|
5121
|
-
const [, , collection, rkey] = uriMatch;
|
|
5917
|
+
const { collection, rkey } = this.parseUri(uri);
|
|
5122
5918
|
if (collection !== HYPERCERT_COLLECTIONS.MEASUREMENT) {
|
|
5123
5919
|
throw new ValidationError(`URI must target a measurement collection. Expected '${HYPERCERT_COLLECTIONS.MEASUREMENT}', got '${collection}'`);
|
|
5124
5920
|
}
|
|
5125
|
-
const
|
|
5126
|
-
|
|
5127
|
-
collection,
|
|
5128
|
-
rkey,
|
|
5129
|
-
});
|
|
5130
|
-
if (!existing.success) {
|
|
5131
|
-
throw new NetworkError(`Measurement not found: ${uri}`);
|
|
5132
|
-
}
|
|
5133
|
-
const recordForUpdate = await this.applyMeasurementUpdates(existing.data.value, updates);
|
|
5921
|
+
const { record: existingRecord } = await this.fetchRecord(uri);
|
|
5922
|
+
const recordForUpdate = await this.applyMeasurementUpdates(existingRecord, updates);
|
|
5134
5923
|
const validation = lexicon.validate(recordForUpdate, HYPERCERT_COLLECTIONS.MEASUREMENT, "main", false);
|
|
5135
5924
|
if (!validation.success) {
|
|
5136
5925
|
throw new ValidationError(`Invalid measurement record: ${validation.error?.message}`);
|
|
5137
5926
|
}
|
|
5138
|
-
const result = await this.
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
rkey,
|
|
5142
|
-
record: recordForUpdate,
|
|
5143
|
-
});
|
|
5144
|
-
if (!result.success) {
|
|
5145
|
-
throw new NetworkError("Failed to update measurement");
|
|
5146
|
-
}
|
|
5147
|
-
this.emit("measurementUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
5148
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
5927
|
+
const result = await this.saveRecord(collection, rkey, recordForUpdate);
|
|
5928
|
+
this.emit("measurementUpdated", { uri: result.uri, cid: result.cid });
|
|
5929
|
+
return result;
|
|
5149
5930
|
}
|
|
5150
5931
|
catch (error) {
|
|
5151
5932
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -5182,7 +5963,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5182
5963
|
const evaluationRecord = {
|
|
5183
5964
|
$type: HYPERCERT_COLLECTIONS.EVALUATION,
|
|
5184
5965
|
subject: { uri: subject.uri, cid: subject.cid },
|
|
5185
|
-
evaluators: params.evaluators,
|
|
5966
|
+
evaluators: params.evaluators.map((evaluator) => ({ did: evaluator })),
|
|
5186
5967
|
summary: params.summary,
|
|
5187
5968
|
createdAt,
|
|
5188
5969
|
};
|
|
@@ -5330,29 +6111,13 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5330
6111
|
*/
|
|
5331
6112
|
async getCollection(uri) {
|
|
5332
6113
|
try {
|
|
5333
|
-
const
|
|
5334
|
-
if (!uriMatch) {
|
|
5335
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5336
|
-
}
|
|
5337
|
-
const [, , collection, rkey] = uriMatch;
|
|
5338
|
-
const result = await this.agent.com.atproto.repo.getRecord({
|
|
5339
|
-
repo: this.repoDid,
|
|
5340
|
-
collection,
|
|
5341
|
-
rkey,
|
|
5342
|
-
});
|
|
5343
|
-
if (!result.success) {
|
|
5344
|
-
throw new NetworkError("Failed to get collection");
|
|
5345
|
-
}
|
|
6114
|
+
const { uri: resultUri, cid, record } = await this.fetchRecord(uri);
|
|
5346
6115
|
// Validate with lexicon registry (more lenient - doesn't require $type)
|
|
5347
|
-
const validation = lexicon.validate(
|
|
6116
|
+
const validation = lexicon.validate(record, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
5348
6117
|
if (!validation.success) {
|
|
5349
6118
|
throw new ValidationError(`Invalid collection record format: ${validation.error?.message}`);
|
|
5350
6119
|
}
|
|
5351
|
-
return {
|
|
5352
|
-
uri: result.data.uri,
|
|
5353
|
-
cid: result.data.cid ?? "",
|
|
5354
|
-
record: result.data.value,
|
|
5355
|
-
};
|
|
6120
|
+
return { uri: resultUri, cid, record };
|
|
5356
6121
|
}
|
|
5357
6122
|
catch (error) {
|
|
5358
6123
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -5366,8 +6131,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5366
6131
|
* @param params - Optional pagination parameters
|
|
5367
6132
|
* @returns Promise resolving to paginated list of collections
|
|
5368
6133
|
* @throws {@link NetworkError} if the list operation fails
|
|
5369
|
-
*
|
|
5370
|
-
* @example
|
|
6134
|
+
* * @example
|
|
5371
6135
|
* ```typescript
|
|
5372
6136
|
* const { records } = await repo.hypercerts.listCollections();
|
|
5373
6137
|
* for (const { record } of records) {
|
|
@@ -5470,36 +6234,17 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5470
6234
|
*/
|
|
5471
6235
|
async getProject(uri) {
|
|
5472
6236
|
try {
|
|
5473
|
-
|
|
5474
|
-
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5475
|
-
if (!uriMatch) {
|
|
5476
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5477
|
-
}
|
|
5478
|
-
const [, , collection, rkey] = uriMatch;
|
|
5479
|
-
// Fetch record
|
|
5480
|
-
const result = await this.agent.com.atproto.repo.getRecord({
|
|
5481
|
-
repo: this.repoDid,
|
|
5482
|
-
collection,
|
|
5483
|
-
rkey,
|
|
5484
|
-
});
|
|
5485
|
-
if (!result.success) {
|
|
5486
|
-
throw new NetworkError("Failed to get project");
|
|
5487
|
-
}
|
|
6237
|
+
const { uri: resultUri, cid, record } = await this.fetchRecord(uri);
|
|
5488
6238
|
// Validate as collection
|
|
5489
|
-
const validation = lexicon.validate(
|
|
6239
|
+
const validation = lexicon.validate(record, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
5490
6240
|
if (!validation.success) {
|
|
5491
6241
|
throw new ValidationError(`Invalid project record format: ${validation.error?.message}`);
|
|
5492
6242
|
}
|
|
5493
6243
|
// Verify it's actually a project (collection with type='project')
|
|
5494
|
-
const record = result.data.value;
|
|
5495
6244
|
if (record.type !== "project") {
|
|
5496
6245
|
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
5497
6246
|
}
|
|
5498
|
-
return {
|
|
5499
|
-
uri: result.data.uri,
|
|
5500
|
-
cid: result.data.cid ?? "",
|
|
5501
|
-
record,
|
|
5502
|
-
};
|
|
6247
|
+
return { uri: resultUri, cid, record };
|
|
5503
6248
|
}
|
|
5504
6249
|
catch (error) {
|
|
5505
6250
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -5594,25 +6339,12 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5594
6339
|
*/
|
|
5595
6340
|
async updateProject(uri, updates) {
|
|
5596
6341
|
// Verify it's a project before updating
|
|
5597
|
-
const
|
|
5598
|
-
if (
|
|
5599
|
-
throw new ValidationError(`
|
|
5600
|
-
}
|
|
5601
|
-
const [, , collection, rkey] = uriMatch;
|
|
5602
|
-
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5603
|
-
repo: this.repoDid,
|
|
5604
|
-
collection,
|
|
5605
|
-
rkey,
|
|
5606
|
-
});
|
|
5607
|
-
if (!existing.success) {
|
|
5608
|
-
throw new NetworkError(`Project not found: ${uri}`);
|
|
6342
|
+
const fetchResult = await this.fetchRecord(uri);
|
|
6343
|
+
if (fetchResult.record.type !== "project") {
|
|
6344
|
+
throw new ValidationError(`Record is not a project (type='${fetchResult.record.type}')`);
|
|
5609
6345
|
}
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
5613
|
-
}
|
|
5614
|
-
// Delegate to updateCollection
|
|
5615
|
-
const result = await this.updateCollection(uri, updates);
|
|
6346
|
+
// Pass pre-fetched record to avoid a second fetch in updateCollectionRecord
|
|
6347
|
+
const result = await this.updateCollectionRecord(fetchResult, updates);
|
|
5616
6348
|
this.emit("projectUpdated", { uri: result.uri, cid: result.cid });
|
|
5617
6349
|
return result;
|
|
5618
6350
|
}
|
|
@@ -5631,20 +6363,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5631
6363
|
* ```
|
|
5632
6364
|
*/
|
|
5633
6365
|
async deleteProject(uri) {
|
|
5634
|
-
const
|
|
5635
|
-
if (!uriMatch) {
|
|
5636
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5637
|
-
}
|
|
5638
|
-
const [, , collection, rkey] = uriMatch;
|
|
5639
|
-
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5640
|
-
repo: this.repoDid,
|
|
5641
|
-
collection,
|
|
5642
|
-
rkey,
|
|
5643
|
-
});
|
|
5644
|
-
if (!existing.success) {
|
|
5645
|
-
throw new NetworkError(`Project not found: ${uri}`);
|
|
5646
|
-
}
|
|
5647
|
-
const record = existing.data.value;
|
|
6366
|
+
const { record } = await this.fetchRecord(uri);
|
|
5648
6367
|
if (record.type !== "project") {
|
|
5649
6368
|
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
5650
6369
|
}
|
|
@@ -5691,21 +6410,25 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5691
6410
|
* ```
|
|
5692
6411
|
*/
|
|
5693
6412
|
async updateCollection(uri, updates) {
|
|
6413
|
+
const fetchResult = await this.fetchRecord(uri);
|
|
6414
|
+
const result = await this.updateCollectionRecord(fetchResult, updates);
|
|
6415
|
+
this.emit("collectionUpdated", { uri: result.uri, cid: result.cid });
|
|
6416
|
+
return result;
|
|
6417
|
+
}
|
|
6418
|
+
/**
|
|
6419
|
+
* Core collection update logic operating on a pre-fetched record.
|
|
6420
|
+
*
|
|
6421
|
+
* Extracted so that callers like {@link updateProject} can fetch once,
|
|
6422
|
+
* validate the type, and then delegate here without a redundant fetch.
|
|
6423
|
+
*
|
|
6424
|
+
* @param fetchResult - The pre-fetched record, collection, and rkey
|
|
6425
|
+
* @param updates - Fields to update (partial)
|
|
6426
|
+
* @returns Promise resolving to updated URI and CID
|
|
6427
|
+
* @internal
|
|
6428
|
+
*/
|
|
6429
|
+
async updateCollectionRecord(fetchResult, updates) {
|
|
5694
6430
|
try {
|
|
5695
|
-
const
|
|
5696
|
-
if (!uriMatch) {
|
|
5697
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5698
|
-
}
|
|
5699
|
-
const [, , collection, rkey] = uriMatch;
|
|
5700
|
-
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5701
|
-
repo: this.repoDid,
|
|
5702
|
-
collection,
|
|
5703
|
-
rkey,
|
|
5704
|
-
});
|
|
5705
|
-
if (!existing.success) {
|
|
5706
|
-
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5707
|
-
}
|
|
5708
|
-
const existingRecord = existing.data.value;
|
|
6431
|
+
const { record: existingRecord, collection, rkey } = fetchResult;
|
|
5709
6432
|
const recordForUpdate = {
|
|
5710
6433
|
...existingRecord,
|
|
5711
6434
|
createdAt: existingRecord.createdAt,
|
|
@@ -5771,17 +6494,8 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5771
6494
|
if (!validation.success) {
|
|
5772
6495
|
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
5773
6496
|
}
|
|
5774
|
-
const result = await this.
|
|
5775
|
-
|
|
5776
|
-
collection,
|
|
5777
|
-
rkey,
|
|
5778
|
-
record: recordForUpdate,
|
|
5779
|
-
});
|
|
5780
|
-
if (!result.success) {
|
|
5781
|
-
throw new NetworkError("Failed to update collection");
|
|
5782
|
-
}
|
|
5783
|
-
this.emit("collectionUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
5784
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
6497
|
+
const result = await this.saveRecord(collection, rkey, recordForUpdate);
|
|
6498
|
+
return result;
|
|
5785
6499
|
}
|
|
5786
6500
|
catch (error) {
|
|
5787
6501
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -5796,11 +6510,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5796
6510
|
*/
|
|
5797
6511
|
async deleteCollection(uri) {
|
|
5798
6512
|
try {
|
|
5799
|
-
const
|
|
5800
|
-
if (!uriMatch) {
|
|
5801
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5802
|
-
}
|
|
5803
|
-
const [, , collection, rkey] = uriMatch;
|
|
6513
|
+
const { collection, rkey } = this.parseUri(uri);
|
|
5804
6514
|
const result = await this.agent.com.atproto.repo.deleteRecord({
|
|
5805
6515
|
repo: this.repoDid,
|
|
5806
6516
|
collection,
|
|
@@ -5826,36 +6536,16 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5826
6536
|
*/
|
|
5827
6537
|
async attachLocationToCollection(uri, location) {
|
|
5828
6538
|
try {
|
|
5829
|
-
const
|
|
5830
|
-
if (!uriMatch) {
|
|
5831
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5832
|
-
}
|
|
5833
|
-
const [, , collection, rkey] = uriMatch;
|
|
5834
|
-
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5835
|
-
repo: this.repoDid,
|
|
5836
|
-
collection,
|
|
5837
|
-
rkey,
|
|
5838
|
-
});
|
|
5839
|
-
if (!existing.success) {
|
|
5840
|
-
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5841
|
-
}
|
|
6539
|
+
const { record, collection, rkey } = await this.fetchRecord(uri);
|
|
5842
6540
|
const resolvedLocation = await this.resolveLocation(location);
|
|
5843
6541
|
if (!resolvedLocation) {
|
|
5844
6542
|
throw new ValidationError("attachLocationToCollection: failed to resolve location");
|
|
5845
6543
|
}
|
|
5846
6544
|
const recordForUpdate = {
|
|
5847
|
-
...
|
|
6545
|
+
...record,
|
|
5848
6546
|
location: resolvedLocation,
|
|
5849
6547
|
};
|
|
5850
|
-
|
|
5851
|
-
repo: this.repoDid,
|
|
5852
|
-
collection,
|
|
5853
|
-
rkey,
|
|
5854
|
-
record: recordForUpdate,
|
|
5855
|
-
});
|
|
5856
|
-
if (!updateResult.success) {
|
|
5857
|
-
throw new NetworkError("Failed to update collection with location");
|
|
5858
|
-
}
|
|
6548
|
+
await this.saveRecord(collection, rkey, recordForUpdate);
|
|
5859
6549
|
this.emit("locationAttachedToCollection", {
|
|
5860
6550
|
uri: resolvedLocation.uri,
|
|
5861
6551
|
cid: resolvedLocation.cid,
|
|
@@ -5876,30 +6566,10 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5876
6566
|
*/
|
|
5877
6567
|
async removeLocationFromCollection(uri) {
|
|
5878
6568
|
try {
|
|
5879
|
-
const
|
|
5880
|
-
|
|
5881
|
-
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5882
|
-
}
|
|
5883
|
-
const [, , collection, rkey] = uriMatch;
|
|
5884
|
-
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5885
|
-
repo: this.repoDid,
|
|
5886
|
-
collection,
|
|
5887
|
-
rkey,
|
|
5888
|
-
});
|
|
5889
|
-
if (!existing.success) {
|
|
5890
|
-
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5891
|
-
}
|
|
5892
|
-
const recordForUpdate = { ...existing.data.value };
|
|
6569
|
+
const { record, collection, rkey } = await this.fetchRecord(uri);
|
|
6570
|
+
const recordForUpdate = { ...record };
|
|
5893
6571
|
delete recordForUpdate.location;
|
|
5894
|
-
|
|
5895
|
-
repo: this.repoDid,
|
|
5896
|
-
collection,
|
|
5897
|
-
rkey,
|
|
5898
|
-
record: recordForUpdate,
|
|
5899
|
-
});
|
|
5900
|
-
if (!result.success) {
|
|
5901
|
-
throw new NetworkError("Failed to remove location from collection");
|
|
5902
|
-
}
|
|
6572
|
+
await this.saveRecord(collection, rkey, recordForUpdate);
|
|
5903
6573
|
this.emit("locationRemovedFromCollection", { collectionUri: uri });
|
|
5904
6574
|
}
|
|
5905
6575
|
catch (error) {
|
|
@@ -6701,7 +7371,7 @@ class OrganizationOperationsImpl {
|
|
|
6701
7371
|
* const repo = sdk.repository(session);
|
|
6702
7372
|
*
|
|
6703
7373
|
* // Access user profile
|
|
6704
|
-
* const
|
|
7374
|
+
* const bskyProfile = await repo.profile.getBskyProfile();
|
|
6705
7375
|
*
|
|
6706
7376
|
* // Create a hypercert
|
|
6707
7377
|
* const result = await repo.hypercerts.create({
|
|
@@ -6725,7 +7395,7 @@ class OrganizationOperationsImpl {
|
|
|
6725
7395
|
*
|
|
6726
7396
|
* // Get another user's repo (read-only for most operations)
|
|
6727
7397
|
* const otherRepo = myRepo.repo("did:plc:other-user-did");
|
|
6728
|
-
* const theirProfile = await otherRepo.profile.
|
|
7398
|
+
* const theirProfile = await otherRepo.profile.getCertifiedProfile();
|
|
6729
7399
|
* ```
|
|
6730
7400
|
*
|
|
6731
7401
|
* @example SDS operations
|
|
@@ -6836,7 +7506,7 @@ class Repository {
|
|
|
6836
7506
|
* ```typescript
|
|
6837
7507
|
* // Read another user's profile
|
|
6838
7508
|
* const otherRepo = repo.repo("did:plc:other-user");
|
|
6839
|
-
* const profile = await otherRepo.profile.
|
|
7509
|
+
* const profile = await otherRepo.profile.getBskyProfile();
|
|
6840
7510
|
*
|
|
6841
7511
|
* // List their public hypercerts
|
|
6842
7512
|
* const hypercerts = await otherRepo.hypercerts.list();
|
|
@@ -6960,21 +7630,22 @@ class Repository {
|
|
|
6960
7630
|
*
|
|
6961
7631
|
* @example
|
|
6962
7632
|
* ```typescript
|
|
6963
|
-
* // Get
|
|
6964
|
-
* const
|
|
6965
|
-
* console.log(
|
|
7633
|
+
* // Get Certified profile (with hypercerts fields)
|
|
7634
|
+
* const certProfile = await repo.profile.getCertifiedProfile();
|
|
7635
|
+
* console.log(certProfile.displayName);
|
|
7636
|
+
* console.log(certProfile.pronouns);
|
|
6966
7637
|
*
|
|
6967
|
-
* // Update profile
|
|
6968
|
-
* await repo.profile.
|
|
7638
|
+
* // Update Certified profile
|
|
7639
|
+
* await repo.profile.updateCertifiedProfile({
|
|
6969
7640
|
* displayName: "New Name",
|
|
6970
|
-
*
|
|
7641
|
+
* pronouns: "they/them",
|
|
6971
7642
|
* avatar: avatarBlob, // Optional: update avatar image
|
|
6972
7643
|
* });
|
|
6973
7644
|
* ```
|
|
6974
7645
|
*/
|
|
6975
7646
|
get profile() {
|
|
6976
7647
|
if (!this._profile) {
|
|
6977
|
-
this._profile = new ProfileOperationsImpl(this.agent, this.repoDid, this.blobs);
|
|
7648
|
+
this._profile = new ProfileOperationsImpl(this.agent, this.repoDid, this.blobs, this.serverUrl);
|
|
6978
7649
|
}
|
|
6979
7650
|
return this._profile;
|
|
6980
7651
|
}
|
|
@@ -7231,25 +7902,11 @@ const OAuthConfigSchema = zod.z.object({
|
|
|
7231
7902
|
* Zod schema for server URL configuration.
|
|
7232
7903
|
*
|
|
7233
7904
|
* @remarks
|
|
7234
|
-
*
|
|
7905
|
+
* Configure SDS here for collaborative operations.
|
|
7906
|
+
* PDS URLs are auto-detected from the user's OAuth session and do not need configuration.
|
|
7235
7907
|
* For local development, HTTP loopback URLs are allowed.
|
|
7236
7908
|
*/
|
|
7237
7909
|
const ServerConfigSchema = zod.z.object({
|
|
7238
|
-
/**
|
|
7239
|
-
* Personal Data Server URL - the user's own AT Protocol server.
|
|
7240
|
-
* This is the primary server for user data operations.
|
|
7241
|
-
*
|
|
7242
|
-
* @example Production
|
|
7243
|
-
* ```typescript
|
|
7244
|
-
* pds: "https://bsky.social"
|
|
7245
|
-
* ```
|
|
7246
|
-
*
|
|
7247
|
-
* @example Local development
|
|
7248
|
-
* ```typescript
|
|
7249
|
-
* pds: "http://localhost:2583"
|
|
7250
|
-
* ```
|
|
7251
|
-
*/
|
|
7252
|
-
pds: urlOrLoopback.optional(),
|
|
7253
7910
|
/**
|
|
7254
7911
|
* Shared Data Server URL - for collaborative data storage.
|
|
7255
7912
|
* Required for collaborator and organization operations.
|
|
@@ -7294,6 +7951,19 @@ const TimeoutConfigSchema = zod.z.object({
|
|
|
7294
7951
|
*/
|
|
7295
7952
|
const ATProtoSDKConfigSchema = zod.z.object({
|
|
7296
7953
|
oauth: OAuthConfigSchema,
|
|
7954
|
+
/**
|
|
7955
|
+
* URL string used for resolving AT Protocol handles to DIDs
|
|
7956
|
+
* during the OAuth authorization flow. This can be any server that speaks
|
|
7957
|
+
* the `com.atproto.identity.resolveHandle` XRPC method.
|
|
7958
|
+
*
|
|
7959
|
+
* If not provided, the `@atproto` library falls back to DNS-based handle resolution.
|
|
7960
|
+
*
|
|
7961
|
+
* @example
|
|
7962
|
+
* ```typescript
|
|
7963
|
+
* handleResolver: "https://pds-eu-west4.test.certified.app"
|
|
7964
|
+
* ```
|
|
7965
|
+
*/
|
|
7966
|
+
handleResolver: urlOrLoopback.optional(),
|
|
7297
7967
|
servers: ServerConfigSchema.optional(),
|
|
7298
7968
|
timeouts: TimeoutConfigSchema.optional(),
|
|
7299
7969
|
});
|
|
@@ -7318,8 +7988,8 @@ const ATProtoSDKConfigSchema = zod.z.object({
|
|
|
7318
7988
|
* jwksUri: "https://my-app.com/.well-known/jwks.json",
|
|
7319
7989
|
* jwkPrivate: process.env.JWK_PRIVATE_KEY!,
|
|
7320
7990
|
* },
|
|
7991
|
+
* handleResolver: "https://bsky.social",
|
|
7321
7992
|
* servers: {
|
|
7322
|
-
* pds: "https://bsky.social",
|
|
7323
7993
|
* sds: "https://sds.hypercerts.org",
|
|
7324
7994
|
* },
|
|
7325
7995
|
* });
|
|
@@ -7371,11 +8041,13 @@ class ATProtoSDK {
|
|
|
7371
8041
|
* jwksUri: "https://my-app.com/.well-known/jwks.json",
|
|
7372
8042
|
* jwkPrivate: privateKeyJwk,
|
|
7373
8043
|
* },
|
|
7374
|
-
*
|
|
8044
|
+
* handleResolver: "https://pds-eu-west4.test.certified.app",
|
|
7375
8045
|
* });
|
|
7376
8046
|
* ```
|
|
7377
8047
|
*/
|
|
7378
8048
|
constructor(config) {
|
|
8049
|
+
/** Cache of session DID → PDS URL, populated during callback/restoreSession */
|
|
8050
|
+
this.sessionPdsMap = new Map();
|
|
7379
8051
|
// Validate configuration
|
|
7380
8052
|
const validationResult = ATProtoSDKConfigSchema.safeParse(config);
|
|
7381
8053
|
if (!validationResult.success) {
|
|
@@ -7464,7 +8136,18 @@ class ATProtoSDK {
|
|
|
7464
8136
|
* ```
|
|
7465
8137
|
*/
|
|
7466
8138
|
async callback(params) {
|
|
7467
|
-
|
|
8139
|
+
const session = await this.oauthClient.callback(params);
|
|
8140
|
+
// Cache the user's actual PDS URL from the token
|
|
8141
|
+
try {
|
|
8142
|
+
const tokenInfo = await session.getTokenInfo();
|
|
8143
|
+
if (tokenInfo.aud) {
|
|
8144
|
+
this.sessionPdsMap.set(session.did, tokenInfo.aud);
|
|
8145
|
+
}
|
|
8146
|
+
}
|
|
8147
|
+
catch {
|
|
8148
|
+
this.logger?.warn?.("Could not resolve PDS URL from session token during callback");
|
|
8149
|
+
}
|
|
8150
|
+
return session;
|
|
7468
8151
|
}
|
|
7469
8152
|
/**
|
|
7470
8153
|
* Restores an existing OAuth session by DID.
|
|
@@ -7498,7 +8181,20 @@ class ATProtoSDK {
|
|
|
7498
8181
|
if (!did || !did.trim()) {
|
|
7499
8182
|
throw new ValidationError("DID is required");
|
|
7500
8183
|
}
|
|
7501
|
-
|
|
8184
|
+
const session = await this.oauthClient.restore(did.trim());
|
|
8185
|
+
if (session) {
|
|
8186
|
+
// Cache the user's actual PDS URL from the token
|
|
8187
|
+
try {
|
|
8188
|
+
const tokenInfo = await session.getTokenInfo();
|
|
8189
|
+
if (tokenInfo.aud) {
|
|
8190
|
+
this.sessionPdsMap.set(session.did, tokenInfo.aud);
|
|
8191
|
+
}
|
|
8192
|
+
}
|
|
8193
|
+
catch {
|
|
8194
|
+
this.logger?.warn?.("Could not resolve PDS URL from session token during restore");
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
return session;
|
|
7502
8198
|
}
|
|
7503
8199
|
/**
|
|
7504
8200
|
* Revokes an OAuth session, logging the user out.
|
|
@@ -7573,13 +8269,9 @@ class ATProtoSDK {
|
|
|
7573
8269
|
throw new ValidationError("Session is required");
|
|
7574
8270
|
}
|
|
7575
8271
|
try {
|
|
7576
|
-
// Determine PDS URL from session or config
|
|
7577
|
-
const pdsUrl = this.config.servers?.pds;
|
|
7578
|
-
if (!pdsUrl) {
|
|
7579
|
-
throw new ValidationError("PDS server URL not configured");
|
|
7580
|
-
}
|
|
7581
8272
|
// Call com.atproto.server.getSession endpoint using session's fetchHandler
|
|
7582
|
-
// which automatically includes proper authorization with DPoP
|
|
8273
|
+
// which automatically includes proper authorization with DPoP.
|
|
8274
|
+
// The session internally routes this to the user's actual PDS via tokenSet.aud.
|
|
7583
8275
|
const response = await session.fetchHandler("/xrpc/com.atproto.server.getSession", {
|
|
7584
8276
|
method: "GET",
|
|
7585
8277
|
headers: {
|
|
@@ -7626,7 +8318,7 @@ class ATProtoSDK {
|
|
|
7626
8318
|
* @example Using default PDS
|
|
7627
8319
|
* ```typescript
|
|
7628
8320
|
* const repo = sdk.repository(session);
|
|
7629
|
-
* const profile = await repo.profile.
|
|
8321
|
+
* const profile = await repo.profile.getBskyProfile();
|
|
7630
8322
|
* ```
|
|
7631
8323
|
*
|
|
7632
8324
|
* @example Using configured SDS
|
|
@@ -7642,7 +8334,7 @@ class ATProtoSDK {
|
|
|
7642
8334
|
* });
|
|
7643
8335
|
* ```
|
|
7644
8336
|
*/
|
|
7645
|
-
repository(session, options) {
|
|
8337
|
+
async repository(session, options) {
|
|
7646
8338
|
if (!session) {
|
|
7647
8339
|
throw new ValidationError("Session is required");
|
|
7648
8340
|
}
|
|
@@ -7664,11 +8356,20 @@ class ATProtoSDK {
|
|
|
7664
8356
|
isSDS = true;
|
|
7665
8357
|
}
|
|
7666
8358
|
else if (options?.server === "pds" || !options?.server) {
|
|
7667
|
-
//
|
|
7668
|
-
|
|
7669
|
-
|
|
8359
|
+
// Auto-detect PDS from cached session info
|
|
8360
|
+
const did = session.did || session.sub;
|
|
8361
|
+
let cachedPds = this.sessionPdsMap.get(did);
|
|
8362
|
+
if (!cachedPds) {
|
|
8363
|
+
// Try to resolve on the fly before giving up
|
|
8364
|
+
try {
|
|
8365
|
+
cachedPds = await this.resolveSessionPds(session);
|
|
8366
|
+
}
|
|
8367
|
+
catch {
|
|
8368
|
+
throw new ValidationError("Could not determine PDS URL. Ensure the session has valid token info, " +
|
|
8369
|
+
"or was created via SDK auth methods (callback/restoreSession).");
|
|
8370
|
+
}
|
|
7670
8371
|
}
|
|
7671
|
-
serverUrl =
|
|
8372
|
+
serverUrl = cachedPds;
|
|
7672
8373
|
isSDS = false;
|
|
7673
8374
|
}
|
|
7674
8375
|
else {
|
|
@@ -7709,12 +8410,52 @@ class ATProtoSDK {
|
|
|
7709
8410
|
return this.lexiconRegistry;
|
|
7710
8411
|
}
|
|
7711
8412
|
/**
|
|
7712
|
-
*
|
|
8413
|
+
* Resolves and caches the PDS URL from a session's token info.
|
|
8414
|
+
*
|
|
8415
|
+
* This method is automatically called during `callback()` and `restoreSession()`.
|
|
8416
|
+
* Use it manually if you have a session obtained outside the SDK's auth flow
|
|
8417
|
+
* and need to enable PDS auto-detection for `repository()`.
|
|
8418
|
+
*
|
|
8419
|
+
* @param session - An authenticated OAuth session
|
|
8420
|
+
* @returns The resolved PDS URL
|
|
8421
|
+
* @throws {@link ValidationError} if the PDS URL cannot be determined from the session
|
|
8422
|
+
*
|
|
8423
|
+
* @example
|
|
8424
|
+
* ```typescript
|
|
8425
|
+
* // For sessions created outside the SDK auth flow
|
|
8426
|
+
* const pdsUrl = await sdk.resolveSessionPds(session);
|
|
8427
|
+
* const repo = sdk.repository(session); // Now works with auto-detected PDS
|
|
8428
|
+
* ```
|
|
8429
|
+
*/
|
|
8430
|
+
async resolveSessionPds(session) {
|
|
8431
|
+
try {
|
|
8432
|
+
const tokenInfo = await session.getTokenInfo();
|
|
8433
|
+
if (tokenInfo.aud) {
|
|
8434
|
+
this.sessionPdsMap.set(session.did, tokenInfo.aud);
|
|
8435
|
+
return tokenInfo.aud;
|
|
8436
|
+
}
|
|
8437
|
+
throw new ValidationError("Could not determine PDS URL from session token info");
|
|
8438
|
+
}
|
|
8439
|
+
catch (error) {
|
|
8440
|
+
if (error instanceof ValidationError) {
|
|
8441
|
+
throw error;
|
|
8442
|
+
}
|
|
8443
|
+
throw new ValidationError("Could not determine PDS URL from session token info", error);
|
|
8444
|
+
}
|
|
8445
|
+
}
|
|
8446
|
+
/**
|
|
8447
|
+
* Gets the cached PDS URL for a specific DID, if available.
|
|
8448
|
+
*
|
|
8449
|
+
* @param did - The user's DID
|
|
8450
|
+
* @returns The cached PDS URL, or `undefined` if not cached
|
|
7713
8451
|
*
|
|
7714
|
-
* @
|
|
8452
|
+
* @deprecated Use `resolveSessionPds()` to resolve and cache PDS URLs.
|
|
8453
|
+
* This getter is provided for backward compatibility with sdk-react.
|
|
7715
8454
|
*/
|
|
7716
8455
|
get pdsUrl() {
|
|
7717
|
-
|
|
8456
|
+
// Return the first cached PDS URL for backward compat with sdk-react
|
|
8457
|
+
const firstEntry = this.sessionPdsMap.values().next();
|
|
8458
|
+
return firstEntry.done ? undefined : firstEntry.value;
|
|
7718
8459
|
}
|
|
7719
8460
|
/**
|
|
7720
8461
|
* The configured SDS (Shared Data Server) URL.
|
|
@@ -7739,7 +8480,7 @@ class ATProtoSDK {
|
|
|
7739
8480
|
*
|
|
7740
8481
|
* const sdk = createATProtoSDK({
|
|
7741
8482
|
* oauth: { ... },
|
|
7742
|
-
*
|
|
8483
|
+
* handleResolver: "https://bsky.social",
|
|
7743
8484
|
* });
|
|
7744
8485
|
* ```
|
|
7745
8486
|
*/
|
|
@@ -8006,300 +8747,79 @@ class BaseOperations {
|
|
|
8006
8747
|
* results of create or update operations.
|
|
8007
8748
|
*
|
|
8008
8749
|
* @param result - Result from a create or update operation
|
|
8009
|
-
* @returns StrongRef object with uri and cid properties
|
|
8010
|
-
*
|
|
8011
|
-
* @example
|
|
8012
|
-
* ```typescript
|
|
8013
|
-
* // Create a project
|
|
8014
|
-
* const projectResult = await this.validateAndCreate("org.myapp.project", projectRecord);
|
|
8015
|
-
*
|
|
8016
|
-
* // Create a task that references the project
|
|
8017
|
-
* const taskRecord = {
|
|
8018
|
-
* $type: "org.myapp.task",
|
|
8019
|
-
* project: this.createStrongRefFromResult(projectResult),
|
|
8020
|
-
* title: "Implement feature",
|
|
8021
|
-
* createdAt: new Date().toISOString(),
|
|
8022
|
-
* };
|
|
8023
|
-
* ```
|
|
8024
|
-
*/
|
|
8025
|
-
createStrongRefFromResult(result) {
|
|
8026
|
-
return { uri: result.uri, cid: result.cid };
|
|
8027
|
-
}
|
|
8028
|
-
/**
|
|
8029
|
-
* Parses an AT-URI to extract its components.
|
|
8030
|
-
*
|
|
8031
|
-
* AT-URIs follow the format: `at://{did}/{collection}/{rkey}`
|
|
8032
|
-
*
|
|
8033
|
-
* @param uri - AT-URI to parse
|
|
8034
|
-
* @returns Object containing did, collection, and rkey
|
|
8035
|
-
* @throws Error if the URI format is invalid
|
|
8036
|
-
*
|
|
8037
|
-
* @example
|
|
8038
|
-
* ```typescript
|
|
8039
|
-
* const { did, collection, rkey } = this.parseAtUri(
|
|
8040
|
-
* "at://did:plc:abc123/org.hypercerts.claim.activity/xyz789"
|
|
8041
|
-
* );
|
|
8042
|
-
* // did: "did:plc:abc123"
|
|
8043
|
-
* // collection: "org.hypercerts.claim.activity"
|
|
8044
|
-
* // rkey: "xyz789"
|
|
8045
|
-
* ```
|
|
8046
|
-
*/
|
|
8047
|
-
parseAtUri(uri) {
|
|
8048
|
-
if (!uri.startsWith("at://")) {
|
|
8049
|
-
throw new Error(`Invalid AT-URI format: ${uri}`);
|
|
8050
|
-
}
|
|
8051
|
-
const parts = uri.slice(5).split("/"); // Remove "at://" and split
|
|
8052
|
-
if (parts.length !== 3) {
|
|
8053
|
-
throw new Error(`Invalid AT-URI format: ${uri}`);
|
|
8054
|
-
}
|
|
8055
|
-
return {
|
|
8056
|
-
did: parts[0],
|
|
8057
|
-
collection: parts[1],
|
|
8058
|
-
rkey: parts[2],
|
|
8059
|
-
};
|
|
8060
|
-
}
|
|
8061
|
-
/**
|
|
8062
|
-
* Builds an AT-URI from its components.
|
|
8063
|
-
*
|
|
8064
|
-
* @param did - DID of the repository
|
|
8065
|
-
* @param collection - NSID of the collection
|
|
8066
|
-
* @param rkey - Record key (typically a TID)
|
|
8067
|
-
* @returns Complete AT-URI string
|
|
8068
|
-
*
|
|
8069
|
-
* @example
|
|
8070
|
-
* ```typescript
|
|
8071
|
-
* const uri = this.buildAtUri(
|
|
8072
|
-
* "did:plc:abc123",
|
|
8073
|
-
* "org.myapp.evaluation",
|
|
8074
|
-
* "xyz789"
|
|
8075
|
-
* );
|
|
8076
|
-
* // Returns: "at://did:plc:abc123/org.myapp.evaluation/xyz789"
|
|
8077
|
-
* ```
|
|
8078
|
-
*/
|
|
8079
|
-
buildAtUri(did, collection, rkey) {
|
|
8080
|
-
return `at://${did}/${collection}/${rkey}`;
|
|
8081
|
-
}
|
|
8082
|
-
}
|
|
8083
|
-
|
|
8084
|
-
/**
|
|
8085
|
-
* Lexicon Development Utilities - AT-URI and StrongRef Helpers
|
|
8086
|
-
*
|
|
8087
|
-
* This module provides utilities for working with AT Protocol URIs and strongRefs
|
|
8088
|
-
* when building custom lexicons. These tools help developers create type-safe
|
|
8089
|
-
* references between records.
|
|
8090
|
-
*
|
|
8091
|
-
* @packageDocumentation
|
|
8092
|
-
*/
|
|
8093
|
-
/**
|
|
8094
|
-
* Parse an AT-URI into its component parts.
|
|
8095
|
-
*
|
|
8096
|
-
* Extracts the DID, collection NSID, and record key from an AT-URI string.
|
|
8097
|
-
* AT-URIs follow the format: `at://{did}/{collection}/{rkey}`
|
|
8098
|
-
*
|
|
8099
|
-
* @param uri - The AT-URI to parse
|
|
8100
|
-
* @returns The components of the URI
|
|
8101
|
-
* @throws {Error} If the URI format is invalid
|
|
8102
|
-
*
|
|
8103
|
-
* @example
|
|
8104
|
-
* ```typescript
|
|
8105
|
-
* const components = parseAtUri("at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a");
|
|
8106
|
-
* console.log(components);
|
|
8107
|
-
* // {
|
|
8108
|
-
* // did: "did:plc:abc123",
|
|
8109
|
-
* // collection: "org.hypercerts.claim.activity",
|
|
8110
|
-
* // rkey: "3km2vj4kfqp2a"
|
|
8111
|
-
* // }
|
|
8112
|
-
* ```
|
|
8113
|
-
*/
|
|
8114
|
-
function parseAtUri(uri) {
|
|
8115
|
-
if (!uri.startsWith("at://")) {
|
|
8116
|
-
throw new Error(`Invalid AT-URI format: must start with "at://", got "${uri}"`);
|
|
8117
|
-
}
|
|
8118
|
-
const withoutProtocol = uri.slice(5); // Remove "at://"
|
|
8119
|
-
const parts = withoutProtocol.split("/");
|
|
8120
|
-
if (parts.length !== 3) {
|
|
8121
|
-
throw new Error(`Invalid AT-URI format: expected "at://{did}/{collection}/{rkey}", got "${uri}"`);
|
|
8122
|
-
}
|
|
8123
|
-
const [did, collection, rkey] = parts;
|
|
8124
|
-
if (!did || !collection || !rkey) {
|
|
8125
|
-
throw new Error(`Invalid AT-URI format: all components must be non-empty, got "${uri}"`);
|
|
8126
|
-
}
|
|
8127
|
-
return { did, collection, rkey };
|
|
8128
|
-
}
|
|
8129
|
-
/**
|
|
8130
|
-
* Build an AT-URI from its component parts.
|
|
8131
|
-
*
|
|
8132
|
-
* Constructs a valid AT-URI string from a DID, collection NSID, and record key.
|
|
8133
|
-
* The resulting URI follows the format: `at://{did}/{collection}/{rkey}`
|
|
8134
|
-
*
|
|
8135
|
-
* @param did - The repository owner's DID
|
|
8136
|
-
* @param collection - The collection NSID (lexicon identifier)
|
|
8137
|
-
* @param rkey - The record key (TID or custom string)
|
|
8138
|
-
* @returns The complete AT-URI
|
|
8139
|
-
*
|
|
8140
|
-
* @example
|
|
8141
|
-
* ```typescript
|
|
8142
|
-
* const uri = buildAtUri(
|
|
8143
|
-
* "did:plc:abc123",
|
|
8144
|
-
* "org.hypercerts.claim.activity",
|
|
8145
|
-
* "3km2vj4kfqp2a"
|
|
8146
|
-
* );
|
|
8147
|
-
* console.log(uri); // "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a"
|
|
8148
|
-
* ```
|
|
8149
|
-
*/
|
|
8150
|
-
function buildAtUri(did, collection, rkey) {
|
|
8151
|
-
if (!did || !collection || !rkey) {
|
|
8152
|
-
throw new Error("All AT-URI components (did, collection, rkey) must be non-empty");
|
|
8153
|
-
}
|
|
8154
|
-
return `at://${did}/${collection}/${rkey}`;
|
|
8155
|
-
}
|
|
8156
|
-
/**
|
|
8157
|
-
* Extract the record key (TID or custom key) from an AT-URI.
|
|
8158
|
-
*
|
|
8159
|
-
* Returns the last component of the AT-URI, which is the record key.
|
|
8160
|
-
* This is equivalent to `parseAtUri(uri).rkey` but more efficient.
|
|
8161
|
-
*
|
|
8162
|
-
* @param uri - The AT-URI to extract from
|
|
8163
|
-
* @returns The record key (TID or custom string)
|
|
8164
|
-
* @throws {Error} If the URI format is invalid
|
|
8165
|
-
*
|
|
8166
|
-
* @example
|
|
8167
|
-
* ```typescript
|
|
8168
|
-
* const rkey = extractRkeyFromUri("at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a");
|
|
8169
|
-
* console.log(rkey); // "3km2vj4kfqp2a"
|
|
8170
|
-
* ```
|
|
8171
|
-
*/
|
|
8172
|
-
function extractRkeyFromUri(uri) {
|
|
8173
|
-
const { rkey } = parseAtUri(uri);
|
|
8174
|
-
return rkey;
|
|
8175
|
-
}
|
|
8176
|
-
/**
|
|
8177
|
-
* Check if a string is a valid AT-URI format.
|
|
8178
|
-
*
|
|
8179
|
-
* Validates that the string follows the AT-URI format without throwing errors.
|
|
8180
|
-
* This is useful for input validation before parsing.
|
|
8181
|
-
*
|
|
8182
|
-
* @param uri - The string to validate
|
|
8183
|
-
* @returns True if the string is a valid AT-URI, false otherwise
|
|
8184
|
-
*
|
|
8185
|
-
* @example
|
|
8186
|
-
* ```typescript
|
|
8187
|
-
* if (isValidAtUri(userInput)) {
|
|
8188
|
-
* const components = parseAtUri(userInput);
|
|
8189
|
-
* // ... use components
|
|
8190
|
-
* } else {
|
|
8191
|
-
* console.error("Invalid AT-URI");
|
|
8192
|
-
* }
|
|
8193
|
-
* ```
|
|
8194
|
-
*/
|
|
8195
|
-
function isValidAtUri(uri) {
|
|
8196
|
-
try {
|
|
8197
|
-
parseAtUri(uri);
|
|
8198
|
-
return true;
|
|
8199
|
-
}
|
|
8200
|
-
catch {
|
|
8201
|
-
return false;
|
|
8202
|
-
}
|
|
8203
|
-
}
|
|
8204
|
-
/**
|
|
8205
|
-
* Create a strongRef from a URI and CID.
|
|
8206
|
-
*
|
|
8207
|
-
* StrongRefs are the canonical way to reference specific versions of records
|
|
8208
|
-
* in AT Protocol. They combine an AT-URI (which identifies the record) with
|
|
8209
|
-
* a CID (which identifies the specific version).
|
|
8210
|
-
*
|
|
8211
|
-
* @param uri - The AT-URI of the record
|
|
8212
|
-
* @param cid - The CID (Content Identifier) of the record version
|
|
8213
|
-
* @returns A strongRef object
|
|
8214
|
-
*
|
|
8215
|
-
* @example
|
|
8216
|
-
* ```typescript
|
|
8217
|
-
* const ref = createStrongRef(
|
|
8218
|
-
* "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a",
|
|
8219
|
-
* "bafyreiabc123..."
|
|
8220
|
-
* );
|
|
8221
|
-
* console.log(ref);
|
|
8222
|
-
* // {
|
|
8223
|
-
* // uri: "at://did:plc:abc123/org.hypercerts.claim.activity/3km2vj4kfqp2a",
|
|
8224
|
-
* // cid: "bafyreiabc123..."
|
|
8225
|
-
* // }
|
|
8226
|
-
* ```
|
|
8227
|
-
*/
|
|
8228
|
-
function createStrongRef(uri, cid) {
|
|
8229
|
-
if (!uri || !cid) {
|
|
8230
|
-
throw new Error("Both uri and cid are required to create a strongRef");
|
|
8750
|
+
* @returns StrongRef object with uri and cid properties
|
|
8751
|
+
*
|
|
8752
|
+
* @example
|
|
8753
|
+
* ```typescript
|
|
8754
|
+
* // Create a project
|
|
8755
|
+
* const projectResult = await this.validateAndCreate("org.myapp.project", projectRecord);
|
|
8756
|
+
*
|
|
8757
|
+
* // Create a task that references the project
|
|
8758
|
+
* const taskRecord = {
|
|
8759
|
+
* $type: "org.myapp.task",
|
|
8760
|
+
* project: this.createStrongRefFromResult(projectResult),
|
|
8761
|
+
* title: "Implement feature",
|
|
8762
|
+
* createdAt: new Date().toISOString(),
|
|
8763
|
+
* };
|
|
8764
|
+
* ```
|
|
8765
|
+
*/
|
|
8766
|
+
createStrongRefFromResult(result) {
|
|
8767
|
+
return { uri: result.uri, cid: result.cid };
|
|
8231
8768
|
}
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8769
|
+
/**
|
|
8770
|
+
* Parses an AT-URI to extract its components.
|
|
8771
|
+
*
|
|
8772
|
+
* AT-URIs follow the format: `at://{did}/{collection}/{rkey}`
|
|
8773
|
+
*
|
|
8774
|
+
* @param uri - AT-URI to parse
|
|
8775
|
+
* @returns Object containing did, collection, and rkey
|
|
8776
|
+
* @throws Error if the URI format is invalid
|
|
8777
|
+
*
|
|
8778
|
+
* @example
|
|
8779
|
+
* ```typescript
|
|
8780
|
+
* const { did, collection, rkey } = this.parseAtUri(
|
|
8781
|
+
* "at://did:plc:abc123/org.hypercerts.claim.activity/xyz789"
|
|
8782
|
+
* );
|
|
8783
|
+
* // did: "did:plc:abc123"
|
|
8784
|
+
* // collection: "org.hypercerts.claim.activity"
|
|
8785
|
+
* // rkey: "xyz789"
|
|
8786
|
+
* ```
|
|
8787
|
+
*/
|
|
8788
|
+
parseAtUri(uri) {
|
|
8789
|
+
if (!uri.startsWith("at://")) {
|
|
8790
|
+
throw new Error(`Invalid AT-URI format: ${uri}`);
|
|
8791
|
+
}
|
|
8792
|
+
const parts = uri.slice(5).split("/"); // Remove "at://" and split
|
|
8793
|
+
if (parts.length !== 3) {
|
|
8794
|
+
throw new Error(`Invalid AT-URI format: ${uri}`);
|
|
8795
|
+
}
|
|
8796
|
+
return {
|
|
8797
|
+
did: parts[0],
|
|
8798
|
+
collection: parts[1],
|
|
8799
|
+
rkey: parts[2],
|
|
8800
|
+
};
|
|
8801
|
+
}
|
|
8802
|
+
/**
|
|
8803
|
+
* Builds an AT-URI from its components.
|
|
8804
|
+
*
|
|
8805
|
+
* @param did - DID of the repository
|
|
8806
|
+
* @param collection - NSID of the collection
|
|
8807
|
+
* @param rkey - Record key (typically a TID)
|
|
8808
|
+
* @returns Complete AT-URI string
|
|
8809
|
+
*
|
|
8810
|
+
* @example
|
|
8811
|
+
* ```typescript
|
|
8812
|
+
* const uri = this.buildAtUri(
|
|
8813
|
+
* "did:plc:abc123",
|
|
8814
|
+
* "org.myapp.evaluation",
|
|
8815
|
+
* "xyz789"
|
|
8816
|
+
* );
|
|
8817
|
+
* // Returns: "at://did:plc:abc123/org.myapp.evaluation/xyz789"
|
|
8818
|
+
* ```
|
|
8819
|
+
*/
|
|
8820
|
+
buildAtUri(did, collection, rkey) {
|
|
8821
|
+
return `at://${did}/${collection}/${rkey}`;
|
|
8278
8822
|
}
|
|
8279
|
-
const obj = ref;
|
|
8280
|
-
return typeof obj.uri === "string" && obj.uri.length > 0 && typeof obj.cid === "string" && obj.cid.length > 0;
|
|
8281
|
-
}
|
|
8282
|
-
/**
|
|
8283
|
-
* Type guard to check if a value is a strongRef.
|
|
8284
|
-
*
|
|
8285
|
-
* This is an alias for `validateStrongRef` that provides better semantics
|
|
8286
|
-
* for type narrowing in TypeScript.
|
|
8287
|
-
*
|
|
8288
|
-
* @param value - The value to check
|
|
8289
|
-
* @returns True if the value is a strongRef, false otherwise
|
|
8290
|
-
*
|
|
8291
|
-
* @example
|
|
8292
|
-
* ```typescript
|
|
8293
|
-
* function processReference(ref: unknown) {
|
|
8294
|
-
* if (isStrongRef(ref)) {
|
|
8295
|
-
* // TypeScript knows ref is StrongRef here
|
|
8296
|
-
* console.log(ref.uri);
|
|
8297
|
-
* }
|
|
8298
|
-
* }
|
|
8299
|
-
* ```
|
|
8300
|
-
*/
|
|
8301
|
-
function isStrongRef(value) {
|
|
8302
|
-
return validateStrongRef(value);
|
|
8303
8823
|
}
|
|
8304
8824
|
|
|
8305
8825
|
/**
|
|
@@ -8834,121 +9354,6 @@ async function batchCreateSidecars(repo, sidecars) {
|
|
|
8834
9354
|
return results;
|
|
8835
9355
|
}
|
|
8836
9356
|
|
|
8837
|
-
/**
|
|
8838
|
-
* Zod schema for collaborator permissions in SDS repositories.
|
|
8839
|
-
*
|
|
8840
|
-
* Defines the granular permissions a collaborator can have on a shared repository.
|
|
8841
|
-
* Permissions follow a hierarchical model where higher-level permissions
|
|
8842
|
-
* typically imply lower-level ones.
|
|
8843
|
-
*/
|
|
8844
|
-
const CollaboratorPermissionsSchema = zod.z.object({
|
|
8845
|
-
/**
|
|
8846
|
-
* Can read/view records in the repository.
|
|
8847
|
-
* This is the most basic permission level.
|
|
8848
|
-
*/
|
|
8849
|
-
read: zod.z.boolean(),
|
|
8850
|
-
/**
|
|
8851
|
-
* Can create new records in the repository.
|
|
8852
|
-
* Typically implies `read` permission.
|
|
8853
|
-
*/
|
|
8854
|
-
create: zod.z.boolean(),
|
|
8855
|
-
/**
|
|
8856
|
-
* Can modify existing records in the repository.
|
|
8857
|
-
* Typically implies `read` and `create` permissions.
|
|
8858
|
-
*/
|
|
8859
|
-
update: zod.z.boolean(),
|
|
8860
|
-
/**
|
|
8861
|
-
* Can delete records from the repository.
|
|
8862
|
-
* Typically implies `read`, `create`, and `update` permissions.
|
|
8863
|
-
*/
|
|
8864
|
-
delete: zod.z.boolean(),
|
|
8865
|
-
/**
|
|
8866
|
-
* Can manage collaborators and their permissions.
|
|
8867
|
-
* Administrative permission that allows inviting/removing collaborators.
|
|
8868
|
-
*/
|
|
8869
|
-
admin: zod.z.boolean(),
|
|
8870
|
-
/**
|
|
8871
|
-
* Full ownership of the repository.
|
|
8872
|
-
* Owners have all permissions and cannot be removed by other admins.
|
|
8873
|
-
* There must always be at least one owner.
|
|
8874
|
-
*/
|
|
8875
|
-
owner: zod.z.boolean(),
|
|
8876
|
-
});
|
|
8877
|
-
/**
|
|
8878
|
-
* Zod schema for SDS organization data.
|
|
8879
|
-
*
|
|
8880
|
-
* Organizations are top-level entities in SDS that can own repositories
|
|
8881
|
-
* and have multiple collaborators with different permission levels.
|
|
8882
|
-
*/
|
|
8883
|
-
const OrganizationSchema = zod.z.object({
|
|
8884
|
-
/**
|
|
8885
|
-
* The organization's DID - unique identifier.
|
|
8886
|
-
* Format: "did:plc:..." or "did:web:..."
|
|
8887
|
-
*/
|
|
8888
|
-
did: zod.z.string(),
|
|
8889
|
-
/**
|
|
8890
|
-
* The organization's handle - human-readable identifier.
|
|
8891
|
-
* Format: "orgname.sds.hypercerts.org" or similar
|
|
8892
|
-
*/
|
|
8893
|
-
handle: zod.z.string(),
|
|
8894
|
-
/**
|
|
8895
|
-
* Display name for the organization.
|
|
8896
|
-
*/
|
|
8897
|
-
name: zod.z.string(),
|
|
8898
|
-
/**
|
|
8899
|
-
* Optional description of the organization's purpose.
|
|
8900
|
-
*/
|
|
8901
|
-
description: zod.z.string().optional(),
|
|
8902
|
-
/**
|
|
8903
|
-
* ISO 8601 timestamp when the organization was created.
|
|
8904
|
-
* Format: "2024-01-15T10:30:00.000Z"
|
|
8905
|
-
*/
|
|
8906
|
-
createdAt: zod.z.string(),
|
|
8907
|
-
/**
|
|
8908
|
-
* The current user's permissions within this organization.
|
|
8909
|
-
*/
|
|
8910
|
-
permissions: CollaboratorPermissionsSchema,
|
|
8911
|
-
/**
|
|
8912
|
-
* How the current user relates to this organization.
|
|
8913
|
-
* - `"owner"`: User created or owns the organization
|
|
8914
|
-
* - `"shared"`: User was invited to collaborate (has permissions)
|
|
8915
|
-
* - `"none"`: User has no access to this organization
|
|
8916
|
-
*/
|
|
8917
|
-
accessType: zod.z.enum(["owner", "shared", "none"]),
|
|
8918
|
-
});
|
|
8919
|
-
/**
|
|
8920
|
-
* Zod schema for collaborator data.
|
|
8921
|
-
*
|
|
8922
|
-
* Represents a user who has been granted access to a shared repository
|
|
8923
|
-
* or organization with specific permissions.
|
|
8924
|
-
*/
|
|
8925
|
-
const CollaboratorSchema = zod.z.object({
|
|
8926
|
-
/**
|
|
8927
|
-
* The collaborator's DID - their unique identifier.
|
|
8928
|
-
* Format: "did:plc:..." or "did:web:..."
|
|
8929
|
-
*/
|
|
8930
|
-
userDid: zod.z.string(),
|
|
8931
|
-
/**
|
|
8932
|
-
* The permissions granted to this collaborator.
|
|
8933
|
-
*/
|
|
8934
|
-
permissions: CollaboratorPermissionsSchema,
|
|
8935
|
-
/**
|
|
8936
|
-
* DID of the user who granted these permissions.
|
|
8937
|
-
* Useful for audit trails.
|
|
8938
|
-
*/
|
|
8939
|
-
grantedBy: zod.z.string(),
|
|
8940
|
-
/**
|
|
8941
|
-
* ISO 8601 timestamp when permissions were granted.
|
|
8942
|
-
* Format: "2024-01-15T10:30:00.000Z"
|
|
8943
|
-
*/
|
|
8944
|
-
grantedAt: zod.z.string(),
|
|
8945
|
-
/**
|
|
8946
|
-
* ISO 8601 timestamp when permissions were revoked, if applicable.
|
|
8947
|
-
* Undefined if the collaborator is still active.
|
|
8948
|
-
*/
|
|
8949
|
-
revokedAt: zod.z.string().optional(),
|
|
8950
|
-
});
|
|
8951
|
-
|
|
8952
9357
|
/**
|
|
8953
9358
|
* Rich text utilities for creating facets from plain text.
|
|
8954
9359
|
*
|
|
@@ -9154,6 +9559,7 @@ exports.ATPROTO_SCOPE = ATPROTO_SCOPE;
|
|
|
9154
9559
|
exports.ATProtoSDK = ATProtoSDK;
|
|
9155
9560
|
exports.ATProtoSDKConfigSchema = ATProtoSDKConfigSchema;
|
|
9156
9561
|
exports.ATProtoSDKError = ATProtoSDKError;
|
|
9562
|
+
exports.AT_URI_REGEX = AT_URI_REGEX;
|
|
9157
9563
|
exports.AccountActionSchema = AccountActionSchema;
|
|
9158
9564
|
exports.AccountAttrSchema = AccountAttrSchema;
|
|
9159
9565
|
exports.AccountPermissionSchema = AccountPermissionSchema;
|
|
@@ -9212,12 +9618,16 @@ exports.createStrongRef = createStrongRef;
|
|
|
9212
9618
|
exports.createStrongRefField = createStrongRefField;
|
|
9213
9619
|
exports.createStrongRefFromResult = createStrongRefFromResult;
|
|
9214
9620
|
exports.createWithSidecars = createWithSidecars;
|
|
9621
|
+
exports.extractCidFromImage = extractCidFromImage;
|
|
9215
9622
|
exports.extractRkeyFromUri = extractRkeyFromUri;
|
|
9623
|
+
exports.getBlobUrl = getBlobUrl;
|
|
9216
9624
|
exports.hasAllPermissions = hasAllPermissions;
|
|
9217
9625
|
exports.hasAnyPermission = hasAnyPermission;
|
|
9218
9626
|
exports.hasPermission = hasPermission;
|
|
9219
9627
|
exports.isStrongRef = isStrongRef;
|
|
9220
9628
|
exports.isValidAtUri = isValidAtUri;
|
|
9629
|
+
exports.isValidDid = isValidDid;
|
|
9630
|
+
exports.isValidUri = isValidUri;
|
|
9221
9631
|
exports.mergeScopes = mergeScopes;
|
|
9222
9632
|
exports.parseAtUri = parseAtUri;
|
|
9223
9633
|
exports.parseScope = parseScope;
|