@hypercerts-org/sdk-core 0.10.0-beta.0 → 0.10.0-beta.2
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 +281 -0
- package/README.md +97 -114
- package/dist/index.cjs +435 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +160 -271
- package/dist/index.mjs +387 -499
- package/dist/index.mjs.map +1 -1
- package/dist/lexicons.cjs +103 -374
- package/dist/lexicons.cjs.map +1 -1
- package/dist/lexicons.d.ts +90 -188
- package/dist/lexicons.mjs +103 -367
- package/dist/lexicons.mjs.map +1 -1
- package/dist/types.cjs +6 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +24 -37
- package/dist/types.mjs +2 -0
- package/dist/types.mjs.map +1 -1
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -53,12 +53,14 @@ const claim = await repo.hypercerts.create({
|
|
|
53
53
|
The SDK supports two types of AT Protocol servers:
|
|
54
54
|
|
|
55
55
|
#### Personal Data Server (PDS)
|
|
56
|
+
|
|
56
57
|
- **Purpose**: User's own data storage (e.g., Bluesky)
|
|
57
58
|
- **Use case**: Individual hypercerts, personal records
|
|
58
59
|
- **Features**: Profile management, basic CRUD operations
|
|
59
60
|
- **Example**: `bsky.social`, any Bluesky PDS
|
|
60
61
|
|
|
61
62
|
#### Shared Data Server (SDS)
|
|
63
|
+
|
|
62
64
|
- **Purpose**: Collaborative data storage with access control
|
|
63
65
|
- **Use case**: Organization hypercerts, team collaboration
|
|
64
66
|
- **Features**: Organizations, multi-user access, role-based permissions
|
|
@@ -84,25 +86,27 @@ await orgRepo.hypercerts.list(); // Queries organization's hypercerts on SDS
|
|
|
84
86
|
The SDK uses a `ConfigurableAgent` to route requests to different servers while maintaining your OAuth authentication:
|
|
85
87
|
|
|
86
88
|
1. **Initial Repository Creation**
|
|
89
|
+
|
|
87
90
|
```typescript
|
|
88
91
|
// User authenticates (OAuth session knows user's PDS)
|
|
89
92
|
const session = await sdk.callback(params);
|
|
90
|
-
|
|
93
|
+
|
|
91
94
|
// Create PDS repository - routes to user's PDS
|
|
92
95
|
const pdsRepo = sdk.repository(session);
|
|
93
|
-
|
|
96
|
+
|
|
94
97
|
// Create SDS repository - routes to SDS server
|
|
95
98
|
const sdsRepo = sdk.repository(session, { server: "sds" });
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
2. **Switching Repositories with `.repo()`**
|
|
102
|
+
|
|
99
103
|
```typescript
|
|
100
104
|
// Start with user's SDS repository
|
|
101
105
|
const userSdsRepo = sdk.repository(session, { server: "sds" });
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
// Switch to organization's repository
|
|
104
108
|
const orgRepo = userSdsRepo.repo("did:plc:org-did");
|
|
105
|
-
|
|
109
|
+
|
|
106
110
|
// All operations on orgRepo still route to SDS, not user's PDS
|
|
107
111
|
await orgRepo.hypercerts.list(); // ✅ Queries SDS
|
|
108
112
|
await orgRepo.collaborators.list(); // ✅ Queries SDS
|
|
@@ -171,33 +175,34 @@ const repo = sdk.getRepository(session);
|
|
|
171
175
|
Control exactly what your app can access using type-safe permission builders:
|
|
172
176
|
|
|
173
177
|
```typescript
|
|
174
|
-
import { PermissionBuilder, ScopePresets, buildScope } from
|
|
178
|
+
import { PermissionBuilder, ScopePresets, buildScope } from "@hypercerts-org/sdk-core";
|
|
175
179
|
|
|
176
180
|
// Use ready-made presets
|
|
177
|
-
const scope = ScopePresets.EMAIL_AND_PROFILE;
|
|
178
|
-
const scope = ScopePresets.POSTING_APP;
|
|
181
|
+
const scope = ScopePresets.EMAIL_AND_PROFILE; // Request email + profile access
|
|
182
|
+
const scope = ScopePresets.POSTING_APP; // Full posting capabilities
|
|
179
183
|
|
|
180
184
|
// Or build custom permissions
|
|
181
185
|
const scope = buildScope(
|
|
182
186
|
new PermissionBuilder()
|
|
183
|
-
.accountEmail(
|
|
184
|
-
.repoWrite(
|
|
185
|
-
.blob([
|
|
186
|
-
.build()
|
|
187
|
+
.accountEmail("read") // Read user's email
|
|
188
|
+
.repoWrite("app.bsky.feed.post") // Create/update posts
|
|
189
|
+
.blob(["image/*", "video/*"]) // Upload media
|
|
190
|
+
.build(),
|
|
187
191
|
);
|
|
188
192
|
|
|
189
193
|
// Use in OAuth configuration
|
|
190
194
|
const sdk = createATProtoSDK({
|
|
191
195
|
oauth: {
|
|
192
|
-
clientId:
|
|
193
|
-
redirectUri:
|
|
194
|
-
scope: scope,
|
|
196
|
+
clientId: "your-client-id",
|
|
197
|
+
redirectUri: "https://your-app.com/callback",
|
|
198
|
+
scope: scope, // Your custom scope
|
|
195
199
|
// ... other config
|
|
196
|
-
}
|
|
200
|
+
},
|
|
197
201
|
});
|
|
198
202
|
```
|
|
199
203
|
|
|
200
204
|
**Available Presets:**
|
|
205
|
+
|
|
201
206
|
- `EMAIL_READ` - User's email address
|
|
202
207
|
- `PROFILE_READ` / `PROFILE_WRITE` - Profile access
|
|
203
208
|
- `POST_WRITE` - Create posts
|
|
@@ -218,7 +223,7 @@ const hypercert = await repo.hypercerts.create({
|
|
|
218
223
|
description: "Research on carbon capture technologies",
|
|
219
224
|
image: imageBlob, // optional: File or Blob
|
|
220
225
|
externalUrl: "https://example.com/project",
|
|
221
|
-
|
|
226
|
+
|
|
222
227
|
impact: {
|
|
223
228
|
scope: ["Climate Change", "Carbon Capture"],
|
|
224
229
|
work: {
|
|
@@ -227,7 +232,7 @@ const hypercert = await repo.hypercerts.create({
|
|
|
227
232
|
},
|
|
228
233
|
contributors: ["did:plc:researcher1", "did:plc:researcher2"],
|
|
229
234
|
},
|
|
230
|
-
|
|
235
|
+
|
|
231
236
|
rights: {
|
|
232
237
|
license: "CC-BY-4.0",
|
|
233
238
|
allowsDerivatives: true,
|
|
@@ -242,9 +247,7 @@ console.log("Created hypercert:", hypercert.uri);
|
|
|
242
247
|
|
|
243
248
|
```typescript
|
|
244
249
|
// Get a specific hypercert by URI
|
|
245
|
-
const hypercert = await repo.hypercerts.get(
|
|
246
|
-
"at://did:plc:user123/org.hypercerts.claim/abc123"
|
|
247
|
-
);
|
|
250
|
+
const hypercert = await repo.hypercerts.get("at://did:plc:user123/org.hypercerts.claim/abc123");
|
|
248
251
|
|
|
249
252
|
// List all hypercerts in the repository
|
|
250
253
|
const { records } = await repo.hypercerts.list();
|
|
@@ -263,26 +266,21 @@ if (cursor) {
|
|
|
263
266
|
|
|
264
267
|
```typescript
|
|
265
268
|
// Update an existing hypercert
|
|
266
|
-
await repo.hypercerts.update(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
},
|
|
276
|
-
}
|
|
277
|
-
);
|
|
269
|
+
await repo.hypercerts.update("at://did:plc:user123/org.hypercerts.claim/abc123", {
|
|
270
|
+
title: "Updated Climate Research Project",
|
|
271
|
+
description: "Expanded scope to include renewable energy",
|
|
272
|
+
impact: {
|
|
273
|
+
scope: ["Climate Change", "Carbon Capture", "Renewable Energy"],
|
|
274
|
+
work: { from: "2024-01-01", to: "2026-12-31" },
|
|
275
|
+
contributors: ["did:plc:researcher1", "did:plc:researcher2"],
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
#### Deleting a Hypercert
|
|
281
281
|
|
|
282
282
|
```typescript
|
|
283
|
-
await repo.hypercerts.delete(
|
|
284
|
-
"at://did:plc:user123/org.hypercerts.claim/abc123"
|
|
285
|
-
);
|
|
283
|
+
await repo.hypercerts.delete("at://did:plc:user123/org.hypercerts.claim/abc123");
|
|
286
284
|
```
|
|
287
285
|
|
|
288
286
|
### 4. Contributions and Measurements
|
|
@@ -323,10 +321,7 @@ const blobResult = await repo.blobs.upload(imageFile);
|
|
|
323
321
|
console.log("Blob uploaded:", blobResult.ref.$link);
|
|
324
322
|
|
|
325
323
|
// Download a blob
|
|
326
|
-
const blobData = await repo.blobs.get(
|
|
327
|
-
"did:plc:user123",
|
|
328
|
-
"bafyreiabc123..."
|
|
329
|
-
);
|
|
324
|
+
const blobData = await repo.blobs.get("did:plc:user123", "bafyreiabc123...");
|
|
330
325
|
```
|
|
331
326
|
|
|
332
327
|
### 6. Organizations (SDS only)
|
|
@@ -491,40 +486,40 @@ await repo.profile.update({
|
|
|
491
486
|
|
|
492
487
|
### Repository Operations
|
|
493
488
|
|
|
494
|
-
| Operation
|
|
495
|
-
|
|
496
|
-
| **Records**
|
|
497
|
-
| Create record
|
|
498
|
-
| Get record
|
|
499
|
-
| Update record
|
|
500
|
-
| Delete record
|
|
501
|
-
| List records
|
|
502
|
-
| **Hypercerts**
|
|
503
|
-
| Create hypercert
|
|
504
|
-
| Get hypercert
|
|
505
|
-
| Update hypercert
|
|
506
|
-
| Delete hypercert
|
|
507
|
-
| List hypercerts
|
|
508
|
-
| Add contribution
|
|
509
|
-
| Add measurement
|
|
510
|
-
| **Blobs**
|
|
511
|
-
| Upload blob
|
|
512
|
-
| Get blob
|
|
513
|
-
| **Profile**
|
|
514
|
-
| Get profile
|
|
515
|
-
| Update profile
|
|
516
|
-
| **Organizations**
|
|
517
|
-
| Create org
|
|
518
|
-
| Get org
|
|
519
|
-
| List orgs
|
|
520
|
-
| **Collaborators**
|
|
521
|
-
| Grant access
|
|
522
|
-
| Revoke access
|
|
523
|
-
| List collaborators | `repo.collaborators.list()`
|
|
524
|
-
| Check access
|
|
525
|
-
| Get role
|
|
526
|
-
| Get permissions
|
|
527
|
-
| Transfer ownership | `repo.collaborators.transferOwnership()` | ❌
|
|
489
|
+
| Operation | Method | PDS | SDS | Returns |
|
|
490
|
+
| ------------------ | ---------------------------------------- | --- | --- | ---------------------------- |
|
|
491
|
+
| **Records** | | | | |
|
|
492
|
+
| Create record | `repo.records.create()` | ✅ | ✅ | `{ uri, cid }` |
|
|
493
|
+
| Get record | `repo.records.get()` | ✅ | ✅ | Record data |
|
|
494
|
+
| Update record | `repo.records.update()` | ✅ | ✅ | `{ uri, cid }` |
|
|
495
|
+
| Delete record | `repo.records.delete()` | ✅ | ✅ | void |
|
|
496
|
+
| List records | `repo.records.list()` | ✅ | ✅ | `{ records, cursor? }` |
|
|
497
|
+
| **Hypercerts** | | | | |
|
|
498
|
+
| Create hypercert | `repo.hypercerts.create()` | ✅ | ✅ | `{ uri, cid, value }` |
|
|
499
|
+
| Get hypercert | `repo.hypercerts.get()` | ✅ | ✅ | Full hypercert |
|
|
500
|
+
| Update hypercert | `repo.hypercerts.update()` | ✅ | ✅ | `{ uri, cid }` |
|
|
501
|
+
| Delete hypercert | `repo.hypercerts.delete()` | ✅ | ✅ | void |
|
|
502
|
+
| List hypercerts | `repo.hypercerts.list()` | ✅ | ✅ | `{ records, cursor? }` |
|
|
503
|
+
| Add contribution | `repo.hypercerts.addContribution()` | ✅ | ✅ | Contribution |
|
|
504
|
+
| Add measurement | `repo.hypercerts.addMeasurement()` | ✅ | ✅ | Measurement |
|
|
505
|
+
| **Blobs** | | | | |
|
|
506
|
+
| Upload blob | `repo.blobs.upload()` | ✅ | ✅ | `{ ref, mimeType, size }` |
|
|
507
|
+
| Get blob | `repo.blobs.get()` | ✅ | ✅ | Blob data |
|
|
508
|
+
| **Profile** | | | | |
|
|
509
|
+
| Get profile | `repo.profile.get()` | ✅ | ❌ | Profile data |
|
|
510
|
+
| Update profile | `repo.profile.update()` | ✅ | ❌ | void |
|
|
511
|
+
| **Organizations** | | | | |
|
|
512
|
+
| Create org | `repo.organizations.create()` | ❌ | ✅ | `{ did, name, ... }` |
|
|
513
|
+
| Get org | `repo.organizations.get()` | ❌ | ✅ | Organization |
|
|
514
|
+
| List orgs | `repo.organizations.list()` | ❌ | ✅ | `{ organizations, cursor? }` |
|
|
515
|
+
| **Collaborators** | | | | |
|
|
516
|
+
| Grant access | `repo.collaborators.grant()` | ❌ | ✅ | void |
|
|
517
|
+
| Revoke access | `repo.collaborators.revoke()` | ❌ | ✅ | void |
|
|
518
|
+
| List collaborators | `repo.collaborators.list()` | ❌ | ✅ | `{ collaborators, cursor? }` |
|
|
519
|
+
| Check access | `repo.collaborators.hasAccess()` | ❌ | ✅ | boolean |
|
|
520
|
+
| Get role | `repo.collaborators.getRole()` | ❌ | ✅ | Role string |
|
|
521
|
+
| Get permissions | `repo.collaborators.getPermissions()` | ❌ | ✅ | Permissions |
|
|
522
|
+
| Transfer ownership | `repo.collaborators.transferOwnership()` | ❌ | ✅ | void |
|
|
528
523
|
|
|
529
524
|
## Type System
|
|
530
525
|
|
|
@@ -549,15 +544,15 @@ if (OrgHypercertsClaim.isRecord(data)) {
|
|
|
549
544
|
}
|
|
550
545
|
```
|
|
551
546
|
|
|
552
|
-
| Lexicon Type
|
|
553
|
-
|
|
554
|
-
| `OrgHypercertsClaim.Main`
|
|
555
|
-
| `OrgHypercertsClaimRights.Main`
|
|
547
|
+
| Lexicon Type | SDK Alias |
|
|
548
|
+
| ------------------------------------- | ----------------------- |
|
|
549
|
+
| `OrgHypercertsClaim.Main` | `HypercertClaim` |
|
|
550
|
+
| `OrgHypercertsClaimRights.Main` | `HypercertRights` |
|
|
556
551
|
| `OrgHypercertsClaimContribution.Main` | `HypercertContribution` |
|
|
557
|
-
| `OrgHypercertsClaimMeasurement.Main`
|
|
558
|
-
| `OrgHypercertsClaimEvaluation.Main`
|
|
559
|
-
| `OrgHypercertsCollection.Main`
|
|
560
|
-
| `AppCertifiedLocation.Main`
|
|
552
|
+
| `OrgHypercertsClaimMeasurement.Main` | `HypercertMeasurement` |
|
|
553
|
+
| `OrgHypercertsClaimEvaluation.Main` | `HypercertEvaluation` |
|
|
554
|
+
| `OrgHypercertsCollection.Main` | `HypercertCollection` |
|
|
555
|
+
| `AppCertifiedLocation.Main` | `HypercertLocation` |
|
|
561
556
|
|
|
562
557
|
## Error Handling
|
|
563
558
|
|
|
@@ -622,6 +617,7 @@ await sdsAgent.com.atproto.repo.listRecords({...});
|
|
|
622
617
|
```
|
|
623
618
|
|
|
624
619
|
This is useful for:
|
|
620
|
+
|
|
625
621
|
- Connecting to multiple SDS instances simultaneously
|
|
626
622
|
- Testing against different server environments
|
|
627
623
|
- Building tools that work across multiple organizations
|
|
@@ -655,7 +651,8 @@ await mockStore.set(mockSession);
|
|
|
655
651
|
|
|
656
652
|
### Working with Lexicons
|
|
657
653
|
|
|
658
|
-
The SDK exports lexicon types and validation utilities from the `@hypercerts-org/lexicon` package for direct record
|
|
654
|
+
The SDK exports lexicon types and validation utilities from the `@hypercerts-org/lexicon` package for direct record
|
|
655
|
+
manipulation and validation.
|
|
659
656
|
|
|
660
657
|
#### Lexicon Types
|
|
661
658
|
|
|
@@ -680,8 +677,8 @@ const claim: HypercertClaim = {
|
|
|
680
677
|
shortDescription: "Urban garden serving 50 families", // REQUIRED
|
|
681
678
|
description: "Detailed description...",
|
|
682
679
|
workScope: "Food Security",
|
|
683
|
-
workTimeFrameFrom: "2024-01-01T00:00:00Z",
|
|
684
|
-
workTimeFrameTo: "2024-12-31T00:00:00Z",
|
|
680
|
+
workTimeFrameFrom: "2024-01-01T00:00:00Z", // Note: Capital 'F'
|
|
681
|
+
workTimeFrameTo: "2024-12-31T00:00:00Z", // Note: Capital 'F'
|
|
685
682
|
rights: { uri: "at://...", cid: "..." },
|
|
686
683
|
createdAt: new Date().toISOString(),
|
|
687
684
|
};
|
|
@@ -692,16 +689,12 @@ const claim: HypercertClaim = {
|
|
|
692
689
|
Validate records before creating them:
|
|
693
690
|
|
|
694
691
|
```typescript
|
|
695
|
-
import {
|
|
696
|
-
validate,
|
|
697
|
-
OrgHypercertsClaim,
|
|
698
|
-
HYPERCERT_COLLECTIONS,
|
|
699
|
-
} from "@hypercerts-org/sdk-core";
|
|
692
|
+
import { validate, OrgHypercertsClaim, HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk-core";
|
|
700
693
|
|
|
701
694
|
// Validate using the lexicon package
|
|
702
695
|
const validation = validate(
|
|
703
|
-
HYPERCERT_COLLECTIONS.CLAIM,
|
|
704
|
-
claim
|
|
696
|
+
HYPERCERT_COLLECTIONS.CLAIM, // "org.hypercerts.claim"
|
|
697
|
+
claim,
|
|
705
698
|
);
|
|
706
699
|
|
|
707
700
|
if (!validation.valid) {
|
|
@@ -718,20 +711,13 @@ const validationResult = OrgHypercertsClaim.validateMain(claim);
|
|
|
718
711
|
For repository-level validation:
|
|
719
712
|
|
|
720
713
|
```typescript
|
|
721
|
-
import {
|
|
722
|
-
LexiconRegistry,
|
|
723
|
-
HYPERCERT_LEXICONS,
|
|
724
|
-
HYPERCERT_COLLECTIONS,
|
|
725
|
-
} from "@hypercerts-org/sdk-core";
|
|
714
|
+
import { LexiconRegistry, HYPERCERT_LEXICONS, HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk-core";
|
|
726
715
|
|
|
727
716
|
const registry = new LexiconRegistry();
|
|
728
717
|
registry.registerLexicons(HYPERCERT_LEXICONS);
|
|
729
718
|
|
|
730
719
|
// Validate a record
|
|
731
|
-
const result = registry.validate(
|
|
732
|
-
HYPERCERT_COLLECTIONS.CLAIM,
|
|
733
|
-
claimData
|
|
734
|
-
);
|
|
720
|
+
const result = registry.validate(HYPERCERT_COLLECTIONS.CLAIM, claimData);
|
|
735
721
|
|
|
736
722
|
if (!result.valid) {
|
|
737
723
|
console.error("Invalid record:", result.error);
|
|
@@ -741,10 +727,7 @@ if (!result.valid) {
|
|
|
741
727
|
#### Creating Records with Proper Types
|
|
742
728
|
|
|
743
729
|
```typescript
|
|
744
|
-
import type {
|
|
745
|
-
HypercertContribution,
|
|
746
|
-
StrongRef,
|
|
747
|
-
} from "@hypercerts-org/sdk-core";
|
|
730
|
+
import type { HypercertContribution, StrongRef } from "@hypercerts-org/sdk-core";
|
|
748
731
|
import { HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk-core";
|
|
749
732
|
|
|
750
733
|
// Create a contribution record
|
|
@@ -757,8 +740,8 @@ const contribution: HypercertContribution = {
|
|
|
757
740
|
contributors: ["did:plc:contributor1", "did:plc:contributor2"],
|
|
758
741
|
role: "implementer",
|
|
759
742
|
description: "On-ground implementation team",
|
|
760
|
-
workTimeframeFrom: "2024-01-01T00:00:00Z",
|
|
761
|
-
workTimeframeTo: "2024-06-30T00:00:00Z",
|
|
743
|
+
workTimeframeFrom: "2024-01-01T00:00:00Z", // Note: lowercase 'f' for contributions
|
|
744
|
+
workTimeframeTo: "2024-06-30T00:00:00Z", // Note: lowercase 'f' for contributions
|
|
762
745
|
createdAt: new Date().toISOString(),
|
|
763
746
|
};
|
|
764
747
|
|
|
@@ -775,14 +758,14 @@ await repo.records.create({
|
|
|
775
758
|
import { HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk-core";
|
|
776
759
|
|
|
777
760
|
// Collection NSIDs
|
|
778
|
-
HYPERCERT_COLLECTIONS.CLAIM
|
|
779
|
-
HYPERCERT_COLLECTIONS.RIGHTS
|
|
780
|
-
HYPERCERT_COLLECTIONS.CONTRIBUTION
|
|
781
|
-
HYPERCERT_COLLECTIONS.MEASUREMENT
|
|
782
|
-
HYPERCERT_COLLECTIONS.EVALUATION
|
|
783
|
-
HYPERCERT_COLLECTIONS.EVIDENCE
|
|
784
|
-
HYPERCERT_COLLECTIONS.COLLECTION
|
|
785
|
-
HYPERCERT_COLLECTIONS.LOCATION
|
|
761
|
+
HYPERCERT_COLLECTIONS.CLAIM; // "org.hypercerts.claim"
|
|
762
|
+
HYPERCERT_COLLECTIONS.RIGHTS; // "org.hypercerts.claim.rights"
|
|
763
|
+
HYPERCERT_COLLECTIONS.CONTRIBUTION; // "org.hypercerts.claim.contribution"
|
|
764
|
+
HYPERCERT_COLLECTIONS.MEASUREMENT; // "org.hypercerts.claim.measurement"
|
|
765
|
+
HYPERCERT_COLLECTIONS.EVALUATION; // "org.hypercerts.claim.evaluation"
|
|
766
|
+
HYPERCERT_COLLECTIONS.EVIDENCE; // "org.hypercerts.claim.evidence"
|
|
767
|
+
HYPERCERT_COLLECTIONS.COLLECTION; // "org.hypercerts.collection"
|
|
768
|
+
HYPERCERT_COLLECTIONS.LOCATION; // "app.certified.location"
|
|
786
769
|
```
|
|
787
770
|
|
|
788
771
|
## Development
|