@hypercerts-org/sdk-core 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +328 -0
  2. package/.turbo/turbo-test.log +118 -0
  3. package/CHANGELOG.md +16 -0
  4. package/LICENSE +21 -0
  5. package/README.md +100 -0
  6. package/dist/errors.cjs +260 -0
  7. package/dist/errors.cjs.map +1 -0
  8. package/dist/errors.d.ts +233 -0
  9. package/dist/errors.mjs +253 -0
  10. package/dist/errors.mjs.map +1 -0
  11. package/dist/index.cjs +4531 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +3430 -0
  14. package/dist/index.mjs +4448 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/lexicons.cjs +420 -0
  17. package/dist/lexicons.cjs.map +1 -0
  18. package/dist/lexicons.d.ts +227 -0
  19. package/dist/lexicons.mjs +410 -0
  20. package/dist/lexicons.mjs.map +1 -0
  21. package/dist/storage.cjs +270 -0
  22. package/dist/storage.cjs.map +1 -0
  23. package/dist/storage.d.ts +474 -0
  24. package/dist/storage.mjs +267 -0
  25. package/dist/storage.mjs.map +1 -0
  26. package/dist/testing.cjs +415 -0
  27. package/dist/testing.cjs.map +1 -0
  28. package/dist/testing.d.ts +928 -0
  29. package/dist/testing.mjs +410 -0
  30. package/dist/testing.mjs.map +1 -0
  31. package/dist/types.cjs +220 -0
  32. package/dist/types.cjs.map +1 -0
  33. package/dist/types.d.ts +2118 -0
  34. package/dist/types.mjs +212 -0
  35. package/dist/types.mjs.map +1 -0
  36. package/eslint.config.mjs +22 -0
  37. package/package.json +90 -0
  38. package/rollup.config.js +75 -0
  39. package/src/auth/OAuthClient.ts +497 -0
  40. package/src/core/SDK.ts +410 -0
  41. package/src/core/config.ts +243 -0
  42. package/src/core/errors.ts +257 -0
  43. package/src/core/interfaces.ts +324 -0
  44. package/src/core/types.ts +281 -0
  45. package/src/errors.ts +57 -0
  46. package/src/index.ts +107 -0
  47. package/src/lexicons.ts +64 -0
  48. package/src/repository/BlobOperationsImpl.ts +199 -0
  49. package/src/repository/CollaboratorOperationsImpl.ts +288 -0
  50. package/src/repository/HypercertOperationsImpl.ts +1146 -0
  51. package/src/repository/LexiconRegistry.ts +332 -0
  52. package/src/repository/OrganizationOperationsImpl.ts +234 -0
  53. package/src/repository/ProfileOperationsImpl.ts +281 -0
  54. package/src/repository/RecordOperationsImpl.ts +340 -0
  55. package/src/repository/Repository.ts +482 -0
  56. package/src/repository/interfaces.ts +868 -0
  57. package/src/repository/types.ts +111 -0
  58. package/src/services/hypercerts/types.ts +87 -0
  59. package/src/storage/InMemorySessionStore.ts +127 -0
  60. package/src/storage/InMemoryStateStore.ts +146 -0
  61. package/src/storage.ts +63 -0
  62. package/src/testing/index.ts +67 -0
  63. package/src/testing/mocks.ts +142 -0
  64. package/src/testing/stores.ts +285 -0
  65. package/src/testing.ts +64 -0
  66. package/src/types.ts +86 -0
  67. package/tests/auth/OAuthClient.test.ts +164 -0
  68. package/tests/core/SDK.test.ts +176 -0
  69. package/tests/core/errors.test.ts +81 -0
  70. package/tests/repository/BlobOperationsImpl.test.ts +154 -0
  71. package/tests/repository/CollaboratorOperationsImpl.test.ts +323 -0
  72. package/tests/repository/HypercertOperationsImpl.test.ts +652 -0
  73. package/tests/repository/LexiconRegistry.test.ts +192 -0
  74. package/tests/repository/OrganizationOperationsImpl.test.ts +242 -0
  75. package/tests/repository/ProfileOperationsImpl.test.ts +254 -0
  76. package/tests/repository/RecordOperationsImpl.test.ts +375 -0
  77. package/tests/repository/Repository.test.ts +149 -0
  78. package/tests/utils/fixtures.ts +117 -0
  79. package/tests/utils/mocks.ts +109 -0
  80. package/tests/utils/repository-fixtures.ts +78 -0
  81. package/tsconfig.json +11 -0
  82. package/tsconfig.tsbuildinfo +1 -0
  83. package/vitest.config.ts +30 -0
@@ -0,0 +1,281 @@
1
+ import type { OAuthSession } from "@atproto/oauth-client-node";
2
+ import { z } from "zod";
3
+
4
+ /**
5
+ * Decentralized Identifier (DID) - a unique, persistent identifier for AT Protocol users.
6
+ *
7
+ * DIDs are the canonical identifier for users in the AT Protocol ecosystem.
8
+ * Unlike handles which can change, DIDs remain constant for the lifetime of an account.
9
+ *
10
+ * @remarks
11
+ * AT Protocol supports multiple DID methods:
12
+ * - `did:plc:` - PLC (Public Ledger of Credentials) DIDs, most common for Bluesky users
13
+ * - `did:web:` - Web DIDs, resolved via HTTPS
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const did: DID = "did:plc:ewvi7nxzyoun6zhxrhs64oiz";
18
+ * const webDid: DID = "did:web:example.com";
19
+ * ```
20
+ *
21
+ * @see https://atproto.com/specs/did for DID specification
22
+ */
23
+ export type DID = string;
24
+
25
+ /**
26
+ * OAuth session with DPoP (Demonstrating Proof of Possession) support.
27
+ *
28
+ * This type represents an authenticated user session. It wraps the
29
+ * `@atproto/oauth-client-node` OAuthSession and contains:
30
+ * - Access token for API requests
31
+ * - Refresh token for obtaining new access tokens
32
+ * - DPoP key pair for proof-of-possession
33
+ * - User's DID and other identity information
34
+ *
35
+ * @remarks
36
+ * Sessions are managed by the SDK and automatically refresh when tokens expire.
37
+ * Store the user's DID to restore sessions later with {@link ATProtoSDK.restoreSession}.
38
+ *
39
+ * Key properties from OAuthSession:
40
+ * - `did` or `sub`: The user's DID
41
+ * - `handle`: The user's handle (e.g., "user.bsky.social")
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const session = await sdk.callback(params);
46
+ *
47
+ * // Access user identity
48
+ * console.log(`Logged in as: ${session.did}`);
49
+ *
50
+ * // Use session for repository operations
51
+ * const repo = sdk.repository(session);
52
+ * ```
53
+ *
54
+ * @see https://atproto.com/specs/oauth for OAuth specification
55
+ */
56
+ export type Session = OAuthSession;
57
+
58
+ /**
59
+ * Zod schema for collaborator permissions in SDS repositories.
60
+ *
61
+ * Defines the granular permissions a collaborator can have on a shared repository.
62
+ * Permissions follow a hierarchical model where higher-level permissions
63
+ * typically imply lower-level ones.
64
+ */
65
+ export const CollaboratorPermissionsSchema = z.object({
66
+ /**
67
+ * Can read/view records in the repository.
68
+ * This is the most basic permission level.
69
+ */
70
+ read: z.boolean(),
71
+
72
+ /**
73
+ * Can create new records in the repository.
74
+ * Typically implies `read` permission.
75
+ */
76
+ create: z.boolean(),
77
+
78
+ /**
79
+ * Can modify existing records in the repository.
80
+ * Typically implies `read` and `create` permissions.
81
+ */
82
+ update: z.boolean(),
83
+
84
+ /**
85
+ * Can delete records from the repository.
86
+ * Typically implies `read`, `create`, and `update` permissions.
87
+ */
88
+ delete: z.boolean(),
89
+
90
+ /**
91
+ * Can manage collaborators and their permissions.
92
+ * Administrative permission that allows inviting/removing collaborators.
93
+ */
94
+ admin: z.boolean(),
95
+
96
+ /**
97
+ * Full ownership of the repository.
98
+ * Owners have all permissions and cannot be removed by other admins.
99
+ * There must always be at least one owner.
100
+ */
101
+ owner: z.boolean(),
102
+ });
103
+
104
+ /**
105
+ * Collaborator permissions for SDS (Shared Data Server) repositories.
106
+ *
107
+ * These permissions control what actions a collaborator can perform
108
+ * on records within a shared repository.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * // Read-only collaborator
113
+ * const readOnlyPerms: CollaboratorPermissions = {
114
+ * read: true,
115
+ * create: false,
116
+ * update: false,
117
+ * delete: false,
118
+ * admin: false,
119
+ * owner: false,
120
+ * };
121
+ *
122
+ * // Editor collaborator
123
+ * const editorPerms: CollaboratorPermissions = {
124
+ * read: true,
125
+ * create: true,
126
+ * update: true,
127
+ * delete: false,
128
+ * admin: false,
129
+ * owner: false,
130
+ * };
131
+ *
132
+ * // Admin collaborator
133
+ * const adminPerms: CollaboratorPermissions = {
134
+ * read: true,
135
+ * create: true,
136
+ * update: true,
137
+ * delete: true,
138
+ * admin: true,
139
+ * owner: false,
140
+ * };
141
+ * ```
142
+ */
143
+ export type CollaboratorPermissions = z.infer<typeof CollaboratorPermissionsSchema>;
144
+
145
+ /**
146
+ * Zod schema for SDS organization data.
147
+ *
148
+ * Organizations are top-level entities in SDS that can own repositories
149
+ * and have multiple collaborators with different permission levels.
150
+ */
151
+ export const OrganizationSchema = z.object({
152
+ /**
153
+ * The organization's DID - unique identifier.
154
+ * Format: "did:plc:..." or "did:web:..."
155
+ */
156
+ did: z.string(),
157
+
158
+ /**
159
+ * The organization's handle - human-readable identifier.
160
+ * Format: "orgname.sds.hypercerts.org" or similar
161
+ */
162
+ handle: z.string(),
163
+
164
+ /**
165
+ * Display name for the organization.
166
+ */
167
+ name: z.string(),
168
+
169
+ /**
170
+ * Optional description of the organization's purpose.
171
+ */
172
+ description: z.string().optional(),
173
+
174
+ /**
175
+ * ISO 8601 timestamp when the organization was created.
176
+ * Format: "2024-01-15T10:30:00.000Z"
177
+ */
178
+ createdAt: z.string(),
179
+
180
+ /**
181
+ * The current user's permissions within this organization.
182
+ */
183
+ permissions: CollaboratorPermissionsSchema,
184
+
185
+ /**
186
+ * How the current user relates to this organization.
187
+ * - `"owner"`: User created or owns the organization
188
+ * - `"collaborator"`: User was invited to collaborate
189
+ */
190
+ accessType: z.enum(["owner", "collaborator"]),
191
+ });
192
+
193
+ /**
194
+ * SDS Organization entity.
195
+ *
196
+ * Represents an organization on a Shared Data Server. Organizations
197
+ * provide a way to group repositories and manage access for teams.
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const org: Organization = {
202
+ * did: "did:plc:org123abc",
203
+ * handle: "my-team.sds.hypercerts.org",
204
+ * name: "My Team",
205
+ * description: "A team working on impact certificates",
206
+ * createdAt: "2024-01-15T10:30:00.000Z",
207
+ * permissions: {
208
+ * read: true,
209
+ * create: true,
210
+ * update: true,
211
+ * delete: true,
212
+ * admin: true,
213
+ * owner: true,
214
+ * },
215
+ * accessType: "owner",
216
+ * };
217
+ * ```
218
+ */
219
+ export type Organization = z.infer<typeof OrganizationSchema>;
220
+
221
+ /**
222
+ * Zod schema for collaborator data.
223
+ *
224
+ * Represents a user who has been granted access to a shared repository
225
+ * or organization with specific permissions.
226
+ */
227
+ export const CollaboratorSchema = z.object({
228
+ /**
229
+ * The collaborator's DID - their unique identifier.
230
+ * Format: "did:plc:..." or "did:web:..."
231
+ */
232
+ userDid: z.string(),
233
+
234
+ /**
235
+ * The permissions granted to this collaborator.
236
+ */
237
+ permissions: CollaboratorPermissionsSchema,
238
+
239
+ /**
240
+ * DID of the user who granted these permissions.
241
+ * Useful for audit trails.
242
+ */
243
+ grantedBy: z.string(),
244
+
245
+ /**
246
+ * ISO 8601 timestamp when permissions were granted.
247
+ * Format: "2024-01-15T10:30:00.000Z"
248
+ */
249
+ grantedAt: z.string(),
250
+
251
+ /**
252
+ * ISO 8601 timestamp when permissions were revoked, if applicable.
253
+ * Undefined if the collaborator is still active.
254
+ */
255
+ revokedAt: z.string().optional(),
256
+ });
257
+
258
+ /**
259
+ * Collaborator information for SDS repositories.
260
+ *
261
+ * Represents a user who has been granted access to collaborate on
262
+ * a shared repository or organization.
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * const collaborator: Collaborator = {
267
+ * userDid: "did:plc:user456def",
268
+ * permissions: {
269
+ * read: true,
270
+ * create: true,
271
+ * update: true,
272
+ * delete: false,
273
+ * admin: false,
274
+ * owner: false,
275
+ * },
276
+ * grantedBy: "did:plc:owner123abc",
277
+ * grantedAt: "2024-02-01T14:00:00.000Z",
278
+ * };
279
+ * ```
280
+ */
281
+ export type Collaborator = z.infer<typeof CollaboratorSchema>;
package/src/errors.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Errors entrypoint - All SDK error classes.
3
+ *
4
+ * This sub-entrypoint exports all error classes used by the SDK.
5
+ * Import from here when you need to catch or check specific error types.
6
+ *
7
+ * @remarks
8
+ * Import from `@hypercerts-org/sdk/errors`:
9
+ *
10
+ * ```typescript
11
+ * import {
12
+ * ATProtoSDKError,
13
+ * AuthenticationError,
14
+ * ValidationError,
15
+ * } from "@hypercerts-org/sdk/errors";
16
+ * ```
17
+ *
18
+ * **Error Hierarchy**:
19
+ * - {@link ATProtoSDKError} - Base class for all SDK errors
20
+ * - {@link AuthenticationError} - OAuth/authentication failures
21
+ * - {@link SessionExpiredError} - Session expired, needs re-auth
22
+ * - {@link ValidationError} - Input/schema validation failures
23
+ * - {@link NetworkError} - Network/server errors
24
+ * - {@link SDSRequiredError} - SDS-only operation on PDS
25
+ *
26
+ * @example Catching specific errors
27
+ * ```typescript
28
+ * import {
29
+ * ATProtoSDKError,
30
+ * AuthenticationError,
31
+ * ValidationError,
32
+ * } from "@hypercerts-org/sdk/errors";
33
+ *
34
+ * try {
35
+ * await sdk.authorize(identifier);
36
+ * } catch (error) {
37
+ * if (error instanceof AuthenticationError) {
38
+ * console.error("Auth failed:", error.message);
39
+ * } else if (error instanceof ValidationError) {
40
+ * console.error("Invalid input:", error.message);
41
+ * } else if (error instanceof ATProtoSDKError) {
42
+ * console.error(`SDK error [${error.code}]:`, error.message);
43
+ * }
44
+ * }
45
+ * ```
46
+ *
47
+ * @packageDocumentation
48
+ */
49
+
50
+ export {
51
+ ATProtoSDKError,
52
+ AuthenticationError,
53
+ SessionExpiredError,
54
+ ValidationError,
55
+ NetworkError,
56
+ SDSRequiredError,
57
+ } from "./core/errors.js";
package/src/index.ts ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Hypercerts SDK - Main entrypoint.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ // Core SDK
8
+ export { ATProtoSDK, createATProtoSDK } from "./core/SDK.js";
9
+ export type { AuthorizeOptions } from "./core/SDK.js";
10
+ export type { ATProtoSDKConfig } from "./core/config.js";
11
+ export type { Session } from "./core/types.js";
12
+
13
+ // Repository (fluent API)
14
+ export { Repository } from "./repository/Repository.js";
15
+ export type {
16
+ RepositoryOptions,
17
+ CreateResult,
18
+ UpdateResult,
19
+ PaginatedList,
20
+ ListParams,
21
+ RepositoryRole,
22
+ RepositoryAccessGrant,
23
+ OrganizationInfo,
24
+ ProgressStep,
25
+ } from "./repository/types.js";
26
+ export type {
27
+ RecordOperations,
28
+ BlobOperations,
29
+ ProfileOperations,
30
+ HypercertOperations,
31
+ HypercertEvents,
32
+ CollaboratorOperations,
33
+ OrganizationOperations,
34
+ CreateHypercertParams,
35
+ CreateHypercertResult,
36
+ } from "./repository/interfaces.js";
37
+
38
+ // Lexicon Registry
39
+ export { LexiconRegistry } from "./repository/LexiconRegistry.js";
40
+ export type { ValidationResult } from "./repository/LexiconRegistry.js";
41
+
42
+ // ============================================================================
43
+ // Lexicon Types and Validation (from @hypercerts-org/lexicon)
44
+ // ============================================================================
45
+
46
+ // Namespaced types with validation functions (isRecord, validateRecord)
47
+ export {
48
+ OrgHypercertsClaim,
49
+ OrgHypercertsClaimRights,
50
+ OrgHypercertsClaimContribution,
51
+ OrgHypercertsClaimMeasurement,
52
+ OrgHypercertsClaimEvaluation,
53
+ OrgHypercertsClaimEvidence,
54
+ OrgHypercertsCollection,
55
+ AppCertifiedLocation,
56
+ ComAtprotoRepoStrongRef,
57
+ // Validation utilities
58
+ validate,
59
+ schemas,
60
+ schemaDict,
61
+ lexicons,
62
+ ids,
63
+ // Lexicon constants
64
+ HYPERCERT_LEXICONS,
65
+ HYPERCERT_COLLECTIONS,
66
+ } from "./services/hypercerts/types.js";
67
+
68
+ // Type-only exports
69
+ export type { AppCertifiedDefs } from "./services/hypercerts/types.js";
70
+
71
+ // Type aliases for generated lexicon types
72
+ export type {
73
+ StrongRef,
74
+ HypercertClaim,
75
+ HypercertRights,
76
+ HypercertContribution,
77
+ HypercertMeasurement,
78
+ HypercertEvaluation,
79
+ HypercertCollection,
80
+ HypercertCollectionClaimItem,
81
+ HypercertLocation,
82
+ // SDK-specific types
83
+ HypercertEvidence,
84
+ HypercertImage,
85
+ BlobRef,
86
+ HypercertWithMetadata,
87
+ } from "./services/hypercerts/types.js";
88
+
89
+ // Errors
90
+ export {
91
+ ATProtoSDKError,
92
+ AuthenticationError,
93
+ SessionExpiredError,
94
+ ValidationError,
95
+ NetworkError,
96
+ SDSRequiredError,
97
+ } from "./core/errors.js";
98
+
99
+ // Storage interfaces and implementations
100
+ export type { SessionStore, StateStore, CacheInterface, LoggerInterface } from "./core/interfaces.js";
101
+ export { InMemorySessionStore } from "./storage/InMemorySessionStore.js";
102
+ export { InMemoryStateStore } from "./storage/InMemoryStateStore.js";
103
+
104
+ // Core types and schemas
105
+ export type { DID, Organization, Collaborator, CollaboratorPermissions } from "./core/types.js";
106
+ export { OrganizationSchema, CollaboratorSchema, CollaboratorPermissionsSchema } from "./core/types.js";
107
+ export { ATProtoSDKConfigSchema, OAuthConfigSchema, ServerConfigSchema, TimeoutConfigSchema } from "./core/config.js";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Lexicons entrypoint - Lexicon definitions and registry.
3
+ *
4
+ * This sub-entrypoint exports the lexicon registry and hypercert
5
+ * lexicon constants for working with AT Protocol record schemas.
6
+ *
7
+ * @remarks
8
+ * Import from `@hypercerts-org/sdk/lexicons`:
9
+ *
10
+ * ```typescript
11
+ * import {
12
+ * LexiconRegistry,
13
+ * HYPERCERT_LEXICONS,
14
+ * HYPERCERT_COLLECTIONS,
15
+ * } from "@hypercerts-org/sdk/lexicons";
16
+ * ```
17
+ *
18
+ * **Exports**:
19
+ * - {@link LexiconRegistry} - Registry for managing and validating lexicons
20
+ * - {@link HYPERCERT_LEXICONS} - Array of all hypercert lexicon documents
21
+ * - {@link HYPERCERT_COLLECTIONS} - Constants for collection NSIDs
22
+ *
23
+ * @example Using collection constants
24
+ * ```typescript
25
+ * import { HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk/lexicons";
26
+ *
27
+ * // List hypercerts using the correct collection name
28
+ * const records = await repo.records.list({
29
+ * collection: HYPERCERT_COLLECTIONS.RECORD,
30
+ * });
31
+ *
32
+ * // List contributions
33
+ * const contributions = await repo.records.list({
34
+ * collection: HYPERCERT_COLLECTIONS.CONTRIBUTION,
35
+ * });
36
+ * ```
37
+ *
38
+ * @example Custom lexicon registration
39
+ * ```typescript
40
+ * import { LexiconRegistry } from "@hypercerts-org/sdk/lexicons";
41
+ *
42
+ * const registry = sdk.getLexiconRegistry();
43
+ *
44
+ * // Register custom lexicon
45
+ * registry.register({
46
+ * lexicon: 1,
47
+ * id: "org.myapp.customRecord",
48
+ * defs: { ... },
49
+ * });
50
+ *
51
+ * // Validate a record
52
+ * const result = registry.validate("org.myapp.customRecord", record);
53
+ * if (!result.valid) {
54
+ * console.error(result.error);
55
+ * }
56
+ * ```
57
+ *
58
+ * @packageDocumentation
59
+ */
60
+
61
+ export { LexiconRegistry } from "./repository/LexiconRegistry.js";
62
+ export type { ValidationResult } from "./repository/LexiconRegistry.js";
63
+
64
+ export { HYPERCERT_LEXICONS, HYPERCERT_COLLECTIONS } from "@hypercerts-org/lexicon";
@@ -0,0 +1,199 @@
1
+ /**
2
+ * BlobOperationsImpl - Blob upload and retrieval operations.
3
+ *
4
+ * This module provides the implementation for AT Protocol blob operations,
5
+ * handling binary data like images and files.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { Agent } from "@atproto/api";
11
+ import { NetworkError } from "../core/errors.js";
12
+ import type { BlobOperations } from "./interfaces.js";
13
+
14
+ /**
15
+ * Implementation of blob operations for binary data handling.
16
+ *
17
+ * Blobs in AT Protocol are content-addressed binary objects stored
18
+ * separately from records. They are referenced in records using a
19
+ * blob reference object with a CID ($link).
20
+ *
21
+ * @remarks
22
+ * This class is typically not instantiated directly. Access it through
23
+ * {@link Repository.blobs}.
24
+ *
25
+ * **Blob Size Limits**: PDS servers typically impose size limits on blobs.
26
+ * Common limits are:
27
+ * - Images: 1MB
28
+ * - Other files: Varies by server configuration
29
+ *
30
+ * **Supported MIME Types**: Any MIME type is technically supported, but
31
+ * servers may reject certain types. Images (JPEG, PNG, GIF, WebP) are
32
+ * universally supported.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // Upload an image blob
37
+ * const imageBlob = new Blob([imageData], { type: "image/jpeg" });
38
+ * const { ref, mimeType, size } = await repo.blobs.upload(imageBlob);
39
+ *
40
+ * // Use the ref in a record
41
+ * await repo.records.create({
42
+ * collection: "org.example.post",
43
+ * record: {
44
+ * text: "Check out this image!",
45
+ * image: ref, // { $link: "bafyrei..." }
46
+ * createdAt: new Date().toISOString(),
47
+ * },
48
+ * });
49
+ * ```
50
+ *
51
+ * @internal
52
+ */
53
+ export class BlobOperationsImpl implements BlobOperations {
54
+ /**
55
+ * Creates a new BlobOperationsImpl.
56
+ *
57
+ * @param agent - AT Protocol Agent for making API calls
58
+ * @param repoDid - DID of the repository (used for blob retrieval)
59
+ * @param _serverUrl - Server URL (reserved for future use)
60
+ *
61
+ * @internal
62
+ */
63
+ constructor(
64
+ private agent: Agent,
65
+ private repoDid: string,
66
+ private _serverUrl: string,
67
+ ) {}
68
+
69
+ /**
70
+ * Uploads a blob to the server.
71
+ *
72
+ * @param blob - The blob to upload (File or Blob object)
73
+ * @returns Promise resolving to blob reference and metadata
74
+ * @throws {@link NetworkError} if the upload fails
75
+ *
76
+ * @remarks
77
+ * The returned `ref` object should be used directly in records to
78
+ * reference the blob. The `$link` property contains the blob's CID.
79
+ *
80
+ * **MIME Type Detection**: If the blob has no type, it defaults to
81
+ * `application/octet-stream`. For best results, always specify the
82
+ * correct MIME type when creating the Blob.
83
+ *
84
+ * @example Uploading an image
85
+ * ```typescript
86
+ * // From a File input
87
+ * const file = fileInput.files[0];
88
+ * const { ref } = await repo.blobs.upload(file);
89
+ *
90
+ * // From raw data
91
+ * const imageBlob = new Blob([uint8Array], { type: "image/png" });
92
+ * const { ref, mimeType, size } = await repo.blobs.upload(imageBlob);
93
+ *
94
+ * console.log(`Uploaded ${size} bytes of ${mimeType}`);
95
+ * console.log(`CID: ${ref.$link}`);
96
+ * ```
97
+ *
98
+ * @example Using in a hypercert
99
+ * ```typescript
100
+ * const coverImage = new Blob([imageData], { type: "image/jpeg" });
101
+ * const { ref } = await repo.blobs.upload(coverImage);
102
+ *
103
+ * // The ref is used directly in the record
104
+ * await repo.hypercerts.create({
105
+ * title: "My Hypercert",
106
+ * // ... other fields
107
+ * image: coverImage, // HypercertOperations handles upload internally
108
+ * });
109
+ * ```
110
+ */
111
+ async upload(blob: Blob): Promise<{ ref: { $link: string }; mimeType: string; size: number }> {
112
+ try {
113
+ const arrayBuffer = await blob.arrayBuffer();
114
+ const uint8Array = new Uint8Array(arrayBuffer);
115
+
116
+ const result = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
117
+ encoding: blob.type || "application/octet-stream",
118
+ });
119
+
120
+ if (!result.success) {
121
+ throw new NetworkError("Failed to upload blob");
122
+ }
123
+
124
+ return {
125
+ ref: result.data.blob.ref,
126
+ mimeType: result.data.blob.mimeType,
127
+ size: result.data.blob.size,
128
+ };
129
+ } catch (error) {
130
+ if (error instanceof NetworkError) throw error;
131
+ throw new NetworkError(
132
+ `Failed to upload blob: ${error instanceof Error ? error.message : "Unknown error"}`,
133
+ error,
134
+ );
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Retrieves a blob by its CID.
140
+ *
141
+ * @param cid - Content Identifier (CID) of the blob, typically from a blob
142
+ * reference's `$link` property
143
+ * @returns Promise resolving to blob data and MIME type
144
+ * @throws {@link NetworkError} if the blob is not found or retrieval fails
145
+ *
146
+ * @remarks
147
+ * The returned data is a Uint8Array which can be converted to other
148
+ * formats as needed (Blob, ArrayBuffer, Base64, etc.).
149
+ *
150
+ * **MIME Type**: The returned MIME type comes from the Content-Type header.
151
+ * If the server doesn't provide one, it defaults to `application/octet-stream`.
152
+ *
153
+ * @example Basic retrieval
154
+ * ```typescript
155
+ * // Get a blob from a record's blob reference
156
+ * const record = await repo.records.get({ collection, rkey });
157
+ * const blobRef = (record.value as any).image;
158
+ *
159
+ * const { data, mimeType } = await repo.blobs.get(blobRef.$link);
160
+ *
161
+ * // Convert to a Blob for use in the browser
162
+ * const blob = new Blob([data], { type: mimeType });
163
+ * const url = URL.createObjectURL(blob);
164
+ * ```
165
+ *
166
+ * @example Displaying an image
167
+ * ```typescript
168
+ * const { data, mimeType } = await repo.blobs.get(imageCid);
169
+ *
170
+ * // Create data URL for <img> src
171
+ * const base64 = btoa(String.fromCharCode(...data));
172
+ * const dataUrl = `data:${mimeType};base64,${base64}`;
173
+ *
174
+ * // Or use object URL
175
+ * const blob = new Blob([data], { type: mimeType });
176
+ * img.src = URL.createObjectURL(blob);
177
+ * ```
178
+ */
179
+ async get(cid: string): Promise<{ data: Uint8Array; mimeType: string }> {
180
+ try {
181
+ const result = await this.agent.com.atproto.sync.getBlob({
182
+ did: this.repoDid,
183
+ cid,
184
+ });
185
+
186
+ if (!result.success) {
187
+ throw new NetworkError("Failed to get blob");
188
+ }
189
+
190
+ return {
191
+ data: result.data,
192
+ mimeType: result.headers["content-type"] || "application/octet-stream",
193
+ };
194
+ } catch (error) {
195
+ if (error instanceof NetworkError) throw error;
196
+ throw new NetworkError(`Failed to get blob: ${error instanceof Error ? error.message : "Unknown error"}`, error);
197
+ }
198
+ }
199
+ }