porting all github actions from bluesky-social/indigo to tangled CI

Compare changes

Choose any two refs to compare.

+31
.github/workflows/sync-internal.yaml
··· 1 + name: Sync to internal repo 2 + 3 + on: 4 + push: 5 + branches: [main] 6 + 7 + jobs: 8 + sync: 9 + runs-on: ubuntu-latest 10 + if: github.repository == 'bluesky-social/indigo' 11 + steps: 12 + - name: Checkout public repo 13 + uses: actions/checkout@v4 14 + with: 15 + fetch-depth: 0 16 + - name: Generate GitHub App Token 17 + id: app-token 18 + uses: actions/create-github-app-token@v1 19 + with: 20 + app-id: ${{ vars.SYNC_INTERNAL_APP_ID }} 21 + private-key: ${{ secrets.SYNC_INTERNAL_PK }} 22 + repositories: indigo-internal 23 + - name: Push to internal repo 24 + env: 25 + TOKEN: ${{ steps.app-token.outputs.token }} 26 + run: | 27 + git config user.name "github-actions" 28 + git config user.email "test@users.noreply.github.com" 29 + git config --unset-all http.https://github.com/.extraheader 30 + git remote add internal https://x-access-token:${TOKEN}@github.com/bluesky-social/indigo-internal.git 31 + git push internal main --force
+1
.gitignore
··· 41 41 # Don't ignore this file itself, or other specific dotfiles 42 42 !.gitignore 43 43 !.github/ 44 + !.tangled/ 44 45 !.golangci.yaml 45 46 46 47 # Don't commit your (default location) creds
+17
.tangled/workflows/build.yml
··· 1 + when: 2 + - event: push 3 + branch: main 4 + 5 + dependencies: 6 + nixpkgs: 7 + - gcc 8 + - gnumake 9 + - go 10 + 11 + steps: 12 + 13 + - name: build 14 + command: make build 15 + 16 + 17 +
+16
.tangled/workflows/lint.yml
··· 1 + when: 2 + - event: push 3 + branch: main 4 + 5 + dependencies: 6 + nixpkgs: 7 + - go 8 + - gnumake 9 + - gcc 10 + 11 + steps: 12 + - name: lint 13 + command: make lint 14 + 15 + 16 +
+29
.tangled/workflows/lint_build_test.yml
··· 1 + when: 2 + - event: push 3 + branch: ["main", "ci"] 4 + 5 + dependencies: 6 + nixpkgs: 7 + - go 8 + - gnumake 9 + - gcc 10 + 11 + steps: 12 + - name: fetch deps 13 + command: go mod tidy 14 + 15 + - name: lint 16 + command: | 17 + ls 18 + echo "$GOPATH" 19 + make lint 20 + 21 + - name: build 22 + command: make build 23 + 24 + - name: test 25 + command: | 26 + make test 27 + 28 + 29 +
+16
.tangled/workflows/test.yml
··· 1 + when: 2 + - event: push 3 + branch: main 4 + 5 + dependencies: 6 + nixpkgs: 7 + - gcc 8 + - gnumake 9 + - go 10 + 11 + steps: 12 + 13 + - name: test 14 + command: make test 15 + 16 +
+1 -1
Makefile
··· 1 1 2 - SHELL = /bin/bash 2 + SHELL = bash 3 3 .SHELLFLAGS = -o pipefail -c 4 4 5 5 # base path for Lexicon document tree (for lexgen)
+11
api/atproto/moderationcreateReport.go
··· 14 14 15 15 // ModerationCreateReport_Input is the input argument to a com.atproto.moderation.createReport call. 16 16 type ModerationCreateReport_Input struct { 17 + ModTool *ModerationCreateReport_ModTool `json:"modTool,omitempty" cborgen:"modTool,omitempty"` 17 18 // reason: Additional context about the content and violation. 18 19 Reason *string `json:"reason,omitempty" cborgen:"reason,omitempty"` 19 20 // reasonType: Indicates the broad category of violation the report is for. ··· 54 55 default: 55 56 return nil 56 57 } 58 + } 59 + 60 + // ModerationCreateReport_ModTool is a "modTool" in the com.atproto.moderation.createReport schema. 61 + // 62 + // Moderation tool information for tracing the source of the action 63 + type ModerationCreateReport_ModTool struct { 64 + // meta: Additional arbitrary metadata about the source 65 + Meta *interface{} `json:"meta,omitempty" cborgen:"meta,omitempty"` 66 + // name: Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome') 67 + Name string `json:"name" cborgen:"name"` 57 68 } 58 69 59 70 // ModerationCreateReport_Output is the output of a com.atproto.moderation.createReport call.
+28
api/bsky/notificationunregisterPush.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package bsky 4 + 5 + // schema: app.bsky.notification.unregisterPush 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // NotificationUnregisterPush_Input is the input argument to a app.bsky.notification.unregisterPush call. 14 + type NotificationUnregisterPush_Input struct { 15 + AppId string `json:"appId" cborgen:"appId"` 16 + Platform string `json:"platform" cborgen:"platform"` 17 + ServiceDid string `json:"serviceDid" cborgen:"serviceDid"` 18 + Token string `json:"token" cborgen:"token"` 19 + } 20 + 21 + // NotificationUnregisterPush calls the XRPC method "app.bsky.notification.unregisterPush". 22 + func NotificationUnregisterPush(ctx context.Context, c util.LexClient, input *NotificationUnregisterPush_Input) error { 23 + if err := c.LexDo(ctx, util.Procedure, "application/json", "app.bsky.notification.unregisterPush", nil, input, nil); err != nil { 24 + return err 25 + } 26 + 27 + return nil 28 + }
+22 -2
api/ozone/moderationdefs.go
··· 359 359 CreatorHandle *string `json:"creatorHandle,omitempty" cborgen:"creatorHandle,omitempty"` 360 360 Event *ModerationDefs_ModEventView_Event `json:"event" cborgen:"event"` 361 361 Id int64 `json:"id" cborgen:"id"` 362 + ModTool *ModerationDefs_ModTool `json:"modTool,omitempty" cborgen:"modTool,omitempty"` 362 363 Subject *ModerationDefs_ModEventView_Subject `json:"subject" cborgen:"subject"` 363 364 SubjectBlobCids []string `json:"subjectBlobCids" cborgen:"subjectBlobCids"` 364 365 SubjectHandle *string `json:"subjectHandle,omitempty" cborgen:"subjectHandle,omitempty"` ··· 843 844 } 844 845 } 845 846 847 + // ModerationDefs_ModTool is a "modTool" in the tools.ozone.moderation.defs schema. 848 + // 849 + // Moderation tool information for tracing the source of the action 850 + type ModerationDefs_ModTool struct { 851 + // meta: Additional arbitrary metadata about the source 852 + Meta *interface{} `json:"meta,omitempty" cborgen:"meta,omitempty"` 853 + // name: Name/identifier of the source (e.g., 'automod', 'ozone/workspace') 854 + Name string `json:"name" cborgen:"name"` 855 + } 856 + 846 857 // ModerationDefs_Moderation is a "moderation" in the tools.ozone.moderation.defs schema. 847 858 type ModerationDefs_Moderation struct { 848 859 SubjectStatus *ModerationDefs_SubjectStatusView `json:"subjectStatus,omitempty" cborgen:"subjectStatus,omitempty"` ··· 1078 1089 } 1079 1090 1080 1091 type ModerationDefs_SubjectStatusView_Subject struct { 1081 - AdminDefs_RepoRef *comatprototypes.AdminDefs_RepoRef 1082 - RepoStrongRef *comatprototypes.RepoStrongRef 1092 + AdminDefs_RepoRef *comatprototypes.AdminDefs_RepoRef 1093 + RepoStrongRef *comatprototypes.RepoStrongRef 1094 + ConvoDefs_MessageRef *chatbskytypes.ConvoDefs_MessageRef 1083 1095 } 1084 1096 1085 1097 func (t *ModerationDefs_SubjectStatusView_Subject) MarshalJSON() ([]byte, error) { ··· 1091 1103 t.RepoStrongRef.LexiconTypeID = "com.atproto.repo.strongRef" 1092 1104 return json.Marshal(t.RepoStrongRef) 1093 1105 } 1106 + if t.ConvoDefs_MessageRef != nil { 1107 + t.ConvoDefs_MessageRef.LexiconTypeID = "chat.bsky.convo.defs#messageRef" 1108 + return json.Marshal(t.ConvoDefs_MessageRef) 1109 + } 1094 1110 return nil, fmt.Errorf("cannot marshal empty enum") 1095 1111 } 1096 1112 func (t *ModerationDefs_SubjectStatusView_Subject) UnmarshalJSON(b []byte) error { ··· 1106 1122 case "com.atproto.repo.strongRef": 1107 1123 t.RepoStrongRef = new(comatprototypes.RepoStrongRef) 1108 1124 return json.Unmarshal(b, t.RepoStrongRef) 1125 + case "chat.bsky.convo.defs#messageRef": 1126 + t.ConvoDefs_MessageRef = new(chatbskytypes.ConvoDefs_MessageRef) 1127 + return json.Unmarshal(b, t.ConvoDefs_MessageRef) 1109 1128 1110 1129 default: 1111 1130 return nil ··· 1116 1135 // 1117 1136 // Detailed view of a subject. For record subjects, the author's repo and profile will be returned. 1118 1137 type ModerationDefs_SubjectView struct { 1138 + Profile *util.LexiconTypeDecoder `json:"profile,omitempty" cborgen:"profile,omitempty"` 1119 1139 Record *ModerationDefs_RecordViewDetail `json:"record,omitempty" cborgen:"record,omitempty"` 1120 1140 Repo *ModerationDefs_RepoViewDetail `json:"repo,omitempty" cborgen:"repo,omitempty"` 1121 1141 Status *ModerationDefs_SubjectStatusView `json:"status,omitempty" cborgen:"status,omitempty"`
+4 -2
api/ozone/moderationemitEvent.go
··· 15 15 16 16 // ModerationEmitEvent_Input is the input argument to a tools.ozone.moderation.emitEvent call. 17 17 type ModerationEmitEvent_Input struct { 18 - CreatedBy string `json:"createdBy" cborgen:"createdBy"` 19 - Event *ModerationEmitEvent_Input_Event `json:"event" cborgen:"event"` 18 + CreatedBy string `json:"createdBy" cborgen:"createdBy"` 19 + Event *ModerationEmitEvent_Input_Event `json:"event" cborgen:"event"` 20 + // externalId: An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject. 21 + ExternalId *string `json:"externalId,omitempty" cborgen:"externalId,omitempty"` 20 22 ModTool *ModerationDefs_ModTool `json:"modTool,omitempty" cborgen:"modTool,omitempty"` 21 23 Subject *ModerationEmitEvent_Input_Subject `json:"subject" cborgen:"subject"` 22 24 SubjectBlobCids []string `json:"subjectBlobCids,omitempty" cborgen:"subjectBlobCids,omitempty"`
+34
api/ozone/safelinkaddRule.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package ozone 4 + 5 + // schema: tools.ozone.safelink.addRule 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // SafelinkAddRule_Input is the input argument to a tools.ozone.safelink.addRule call. 14 + type SafelinkAddRule_Input struct { 15 + Action *string `json:"action" cborgen:"action"` 16 + // comment: Optional comment about the decision 17 + Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"` 18 + // createdBy: Author DID. Only respected when using admin auth 19 + CreatedBy *string `json:"createdBy,omitempty" cborgen:"createdBy,omitempty"` 20 + Pattern *string `json:"pattern" cborgen:"pattern"` 21 + Reason *string `json:"reason" cborgen:"reason"` 22 + // url: The URL or domain to apply the rule to 23 + Url string `json:"url" cborgen:"url"` 24 + } 25 + 26 + // SafelinkAddRule calls the XRPC method "tools.ozone.safelink.addRule". 27 + func SafelinkAddRule(ctx context.Context, c util.LexClient, input *SafelinkAddRule_Input) (*SafelinkDefs_Event, error) { 28 + var out SafelinkDefs_Event 29 + if err := c.LexDo(ctx, util.Procedure, "application/json", "tools.ozone.safelink.addRule", nil, input, &out); err != nil { 30 + return nil, err 31 + } 32 + 33 + return &out, nil 34 + }
+43
api/ozone/safelinkdefs.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package ozone 4 + 5 + // schema: tools.ozone.safelink.defs 6 + 7 + // SafelinkDefs_Event is a "event" in the tools.ozone.safelink.defs schema. 8 + // 9 + // An event for URL safety decisions 10 + type SafelinkDefs_Event struct { 11 + Action *string `json:"action" cborgen:"action"` 12 + // comment: Optional comment about the decision 13 + Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"` 14 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 15 + // createdBy: DID of the user who created this rule 16 + CreatedBy string `json:"createdBy" cborgen:"createdBy"` 17 + EventType *string `json:"eventType" cborgen:"eventType"` 18 + // id: Auto-incrementing row ID 19 + Id int64 `json:"id" cborgen:"id"` 20 + Pattern *string `json:"pattern" cborgen:"pattern"` 21 + Reason *string `json:"reason" cborgen:"reason"` 22 + // url: The URL that this rule applies to 23 + Url string `json:"url" cborgen:"url"` 24 + } 25 + 26 + // SafelinkDefs_UrlRule is a "urlRule" in the tools.ozone.safelink.defs schema. 27 + // 28 + // Input for creating a URL safety rule 29 + type SafelinkDefs_UrlRule struct { 30 + Action *string `json:"action" cborgen:"action"` 31 + // comment: Optional comment about the decision 32 + Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"` 33 + // createdAt: Timestamp when the rule was created 34 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 35 + // createdBy: DID of the user added the rule. 36 + CreatedBy string `json:"createdBy" cborgen:"createdBy"` 37 + Pattern *string `json:"pattern" cborgen:"pattern"` 38 + Reason *string `json:"reason" cborgen:"reason"` 39 + // updatedAt: Timestamp when the rule was last updated 40 + UpdatedAt string `json:"updatedAt" cborgen:"updatedAt"` 41 + // url: The URL or domain to apply the rule to 42 + Url string `json:"url" cborgen:"url"` 43 + }
+42
api/ozone/safelinkqueryEvents.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package ozone 4 + 5 + // schema: tools.ozone.safelink.queryEvents 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // SafelinkQueryEvents_Input is the input argument to a tools.ozone.safelink.queryEvents call. 14 + type SafelinkQueryEvents_Input struct { 15 + // cursor: Cursor for pagination 16 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 17 + // limit: Maximum number of results to return 18 + Limit *int64 `json:"limit,omitempty" cborgen:"limit,omitempty"` 19 + // patternType: Filter by pattern type 20 + PatternType *string `json:"patternType,omitempty" cborgen:"patternType,omitempty"` 21 + // sortDirection: Sort direction 22 + SortDirection *string `json:"sortDirection,omitempty" cborgen:"sortDirection,omitempty"` 23 + // urls: Filter by specific URLs or domains 24 + Urls []string `json:"urls,omitempty" cborgen:"urls,omitempty"` 25 + } 26 + 27 + // SafelinkQueryEvents_Output is the output of a tools.ozone.safelink.queryEvents call. 28 + type SafelinkQueryEvents_Output struct { 29 + // cursor: Next cursor for pagination. Only present if there are more results. 30 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 31 + Events []*SafelinkDefs_Event `json:"events" cborgen:"events"` 32 + } 33 + 34 + // SafelinkQueryEvents calls the XRPC method "tools.ozone.safelink.queryEvents". 35 + func SafelinkQueryEvents(ctx context.Context, c util.LexClient, input *SafelinkQueryEvents_Input) (*SafelinkQueryEvents_Output, error) { 36 + var out SafelinkQueryEvents_Output 37 + if err := c.LexDo(ctx, util.Procedure, "application/json", "tools.ozone.safelink.queryEvents", nil, input, &out); err != nil { 38 + return nil, err 39 + } 40 + 41 + return &out, nil 42 + }
+48
api/ozone/safelinkqueryRules.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package ozone 4 + 5 + // schema: tools.ozone.safelink.queryRules 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // SafelinkQueryRules_Input is the input argument to a tools.ozone.safelink.queryRules call. 14 + type SafelinkQueryRules_Input struct { 15 + // actions: Filter by action types 16 + Actions []string `json:"actions,omitempty" cborgen:"actions,omitempty"` 17 + // createdBy: Filter by rule creator 18 + CreatedBy *string `json:"createdBy,omitempty" cborgen:"createdBy,omitempty"` 19 + // cursor: Cursor for pagination 20 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 21 + // limit: Maximum number of results to return 22 + Limit *int64 `json:"limit,omitempty" cborgen:"limit,omitempty"` 23 + // patternType: Filter by pattern type 24 + PatternType *string `json:"patternType,omitempty" cborgen:"patternType,omitempty"` 25 + // reason: Filter by reason type 26 + Reason *string `json:"reason,omitempty" cborgen:"reason,omitempty"` 27 + // sortDirection: Sort direction 28 + SortDirection *string `json:"sortDirection,omitempty" cborgen:"sortDirection,omitempty"` 29 + // urls: Filter by specific URLs or domains 30 + Urls []string `json:"urls,omitempty" cborgen:"urls,omitempty"` 31 + } 32 + 33 + // SafelinkQueryRules_Output is the output of a tools.ozone.safelink.queryRules call. 34 + type SafelinkQueryRules_Output struct { 35 + // cursor: Next cursor for pagination. Only present if there are more results. 36 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 37 + Rules []*SafelinkDefs_UrlRule `json:"rules" cborgen:"rules"` 38 + } 39 + 40 + // SafelinkQueryRules calls the XRPC method "tools.ozone.safelink.queryRules". 41 + func SafelinkQueryRules(ctx context.Context, c util.LexClient, input *SafelinkQueryRules_Input) (*SafelinkQueryRules_Output, error) { 42 + var out SafelinkQueryRules_Output 43 + if err := c.LexDo(ctx, util.Procedure, "application/json", "tools.ozone.safelink.queryRules", nil, input, &out); err != nil { 44 + return nil, err 45 + } 46 + 47 + return &out, nil 48 + }
+32
api/ozone/safelinkremoveRule.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package ozone 4 + 5 + // schema: tools.ozone.safelink.removeRule 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // SafelinkRemoveRule_Input is the input argument to a tools.ozone.safelink.removeRule call. 14 + type SafelinkRemoveRule_Input struct { 15 + // comment: Optional comment about why the rule is being removed 16 + Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"` 17 + // createdBy: Optional DID of the user. Only respected when using admin auth. 18 + CreatedBy *string `json:"createdBy,omitempty" cborgen:"createdBy,omitempty"` 19 + Pattern *string `json:"pattern" cborgen:"pattern"` 20 + // url: The URL or domain to remove the rule for 21 + Url string `json:"url" cborgen:"url"` 22 + } 23 + 24 + // SafelinkRemoveRule calls the XRPC method "tools.ozone.safelink.removeRule". 25 + func SafelinkRemoveRule(ctx context.Context, c util.LexClient, input *SafelinkRemoveRule_Input) (*SafelinkDefs_Event, error) { 26 + var out SafelinkDefs_Event 27 + if err := c.LexDo(ctx, util.Procedure, "application/json", "tools.ozone.safelink.removeRule", nil, input, &out); err != nil { 28 + return nil, err 29 + } 30 + 31 + return &out, nil 32 + }
+34
api/ozone/safelinkupdateRule.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package ozone 4 + 5 + // schema: tools.ozone.safelink.updateRule 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // SafelinkUpdateRule_Input is the input argument to a tools.ozone.safelink.updateRule call. 14 + type SafelinkUpdateRule_Input struct { 15 + Action *string `json:"action" cborgen:"action"` 16 + // comment: Optional comment about the update 17 + Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"` 18 + // createdBy: Optional DID to credit as the creator. Only respected for admin_token authentication. 19 + CreatedBy *string `json:"createdBy,omitempty" cborgen:"createdBy,omitempty"` 20 + Pattern *string `json:"pattern" cborgen:"pattern"` 21 + Reason *string `json:"reason" cborgen:"reason"` 22 + // url: The URL or domain to update the rule for 23 + Url string `json:"url" cborgen:"url"` 24 + } 25 + 26 + // SafelinkUpdateRule calls the XRPC method "tools.ozone.safelink.updateRule". 27 + func SafelinkUpdateRule(ctx context.Context, c util.LexClient, input *SafelinkUpdateRule_Input) (*SafelinkDefs_Event, error) { 28 + var out SafelinkDefs_Event 29 + if err := c.LexDo(ctx, util.Procedure, "application/json", "tools.ozone.safelink.updateRule", nil, input, &out); err != nil { 30 + return nil, err 31 + } 32 + 33 + return &out, nil 34 + }
+6 -4
api/ozone/verificationdefs.go
··· 22 22 // handle: Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. 23 23 Handle string `json:"handle" cborgen:"handle"` 24 24 // issuer: The user who issued this verification. 25 - Issuer string `json:"issuer" cborgen:"issuer"` 26 - IssuerRepo *VerificationDefs_VerificationView_IssuerRepo `json:"issuerRepo,omitempty" cborgen:"issuerRepo,omitempty"` 25 + Issuer string `json:"issuer" cborgen:"issuer"` 26 + IssuerProfile *util.LexiconTypeDecoder `json:"issuerProfile,omitempty" cborgen:"issuerProfile,omitempty"` 27 + IssuerRepo *VerificationDefs_VerificationView_IssuerRepo `json:"issuerRepo,omitempty" cborgen:"issuerRepo,omitempty"` 27 28 // revokeReason: Describes the reason for revocation, also indicating that the verification is no longer valid. 28 29 RevokeReason *string `json:"revokeReason,omitempty" cborgen:"revokeReason,omitempty"` 29 30 // revokedAt: Timestamp when the verification was revoked. ··· 31 32 // revokedBy: The user who revoked this verification. 32 33 RevokedBy *string `json:"revokedBy,omitempty" cborgen:"revokedBy,omitempty"` 33 34 // subject: The subject of the verification. 34 - Subject string `json:"subject" cborgen:"subject"` 35 - SubjectRepo *VerificationDefs_VerificationView_SubjectRepo `json:"subjectRepo,omitempty" cborgen:"subjectRepo,omitempty"` 35 + Subject string `json:"subject" cborgen:"subject"` 36 + SubjectProfile *util.LexiconTypeDecoder `json:"subjectProfile,omitempty" cborgen:"subjectProfile,omitempty"` 37 + SubjectRepo *VerificationDefs_VerificationView_SubjectRepo `json:"subjectRepo,omitempty" cborgen:"subjectRepo,omitempty"` 36 38 // uri: The AT-URI of the verification record. 37 39 Uri string `json:"uri" cborgen:"uri"` 38 40 }
+27
atproto/identity/mock_directory.go
··· 4 4 "context" 5 5 "encoding/json" 6 6 "fmt" 7 + "sync" 7 8 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 9 10 ) 10 11 11 12 // A fake identity directory, for use in tests 12 13 type MockDirectory struct { 14 + mu *sync.RWMutex 13 15 Handles map[syntax.Handle]syntax.DID 14 16 Identities map[syntax.DID]Identity 15 17 } ··· 19 21 20 22 func NewMockDirectory() MockDirectory { 21 23 return MockDirectory{ 24 + mu: &sync.RWMutex{}, 22 25 Handles: make(map[syntax.Handle]syntax.DID), 23 26 Identities: make(map[syntax.DID]Identity), 24 27 } 25 28 } 26 29 27 30 func (d *MockDirectory) Insert(ident Identity) { 31 + d.mu.Lock() 32 + defer d.mu.Unlock() 33 + 28 34 if !ident.Handle.IsInvalidHandle() { 29 35 d.Handles[ident.Handle.Normalize()] = ident.DID 30 36 } ··· 32 38 } 33 39 34 40 func (d *MockDirectory) LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error) { 41 + d.mu.RLock() 42 + defer d.mu.RUnlock() 43 + 35 44 h = h.Normalize() 36 45 did, ok := d.Handles[h] 37 46 if !ok { ··· 45 54 } 46 55 47 56 func (d *MockDirectory) LookupDID(ctx context.Context, did syntax.DID) (*Identity, error) { 57 + d.mu.RLock() 58 + defer d.mu.RUnlock() 59 + 48 60 ident, ok := d.Identities[did] 49 61 if !ok { 50 62 return nil, ErrDIDNotFound ··· 53 65 } 54 66 55 67 func (d *MockDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*Identity, error) { 68 + d.mu.RLock() 69 + defer d.mu.RUnlock() 70 + 56 71 handle, err := a.AsHandle() 57 72 if nil == err { // if not an error, is a Handle 58 73 return d.LookupHandle(ctx, handle) ··· 65 80 } 66 81 67 82 func (d *MockDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { 83 + d.mu.RLock() 84 + defer d.mu.RUnlock() 85 + 68 86 h = h.Normalize() 69 87 did, ok := d.Handles[h] 70 88 if !ok { ··· 74 92 } 75 93 76 94 func (d *MockDirectory) ResolveDID(ctx context.Context, did syntax.DID) (*DIDDocument, error) { 95 + d.mu.RLock() 96 + defer d.mu.RUnlock() 97 + 77 98 ident, ok := d.Identities[did] 78 99 if !ok { 79 100 return nil, ErrDIDNotFound ··· 83 104 } 84 105 85 106 func (d *MockDirectory) ResolveDIDRaw(ctx context.Context, did syntax.DID) (json.RawMessage, error) { 107 + d.mu.RLock() 108 + defer d.mu.RUnlock() 109 + 86 110 ident, ok := d.Identities[did] 87 111 if !ok { 88 112 return nil, ErrDIDNotFound ··· 92 116 } 93 117 94 118 func (d *MockDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error { 119 + d.mu.Lock() 120 + defer d.mu.Unlock() 121 + 95 122 return nil 96 123 }
+2
atproto/label/label.go
··· 140 140 Cid: l.CID, 141 141 Cts: l.CreatedAt, 142 142 Exp: l.ExpiresAt, 143 + Neg: l.Negated, 143 144 Sig: []byte(l.Sig), 144 145 Src: l.SourceDID, 145 146 Uri: l.URI, ··· 157 158 CID: l.Cid, 158 159 CreatedAt: l.Cts, 159 160 ExpiresAt: l.Exp, 161 + Negated: l.Neg, 160 162 Sig: []byte(l.Sig), 161 163 SourceDID: l.Src, 162 164 URI: l.Uri,
+62
atproto/label/label_test.go
··· 4 4 "encoding/json" 5 5 "testing" 6 6 7 + comatproto "github.com/bluesky-social/indigo/api/atproto" 7 8 "github.com/bluesky-social/indigo/atproto/crypto" 8 9 9 10 "github.com/stretchr/testify/assert" ··· 89 90 assert.NoError(l.Sign(priv)) 90 91 assert.NoError(l.VerifySignature(pub)) 91 92 } 93 + 94 + func TestToLexicon(t *testing.T) { 95 + assert := assert.New(t) 96 + 97 + expiresAt := "2025-07-28T23:53:19.804Z" 98 + negated := true 99 + cid := "bafyreifxykqhed72s26cr4i64rxvrtofeqrly3j4vjzbkvo3ckkjbxjqtq" 100 + 101 + l := Label{ 102 + CID: &cid, 103 + CreatedAt: "2024-10-23T17:51:19.128Z", 104 + ExpiresAt: &expiresAt, 105 + Negated: &negated, 106 + SourceDID: "did:plc:ewvi7nxzyoun6zhxrhs64oiz", 107 + URI: "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.actor.profile/self", 108 + Val: "good", 109 + Version: ATPROTO_LABEL_VERSION, 110 + Sig: []byte("sig"), // invalid, but we only care about the conversion 111 + } 112 + 113 + lex := l.ToLexicon() 114 + assert.Equal(l.Version, *lex.Ver) 115 + assert.Equal(l.CreatedAt, lex.Cts) 116 + assert.Equal(l.URI, lex.Uri) 117 + assert.Equal(l.Val, lex.Val) 118 + assert.Equal(l.CID, lex.Cid) 119 + assert.Equal(l.ExpiresAt, lex.Exp) 120 + assert.Equal(l.Negated, lex.Neg) 121 + assert.Equal(l.SourceDID, lex.Src) 122 + } 123 + 124 + func TestFromLexicon(t *testing.T) { 125 + assert := assert.New(t) 126 + 127 + expiresAt := "2025-07-28T23:53:19.804Z" 128 + negated := true 129 + cid := "bafyreifxykqhed72s26cr4i64rxvrtofeqrly3j4vjzbkvo3ckkjbxjqtq" 130 + version := int64(1) 131 + 132 + lex := &comatproto.LabelDefs_Label{ 133 + Cid: &cid, 134 + Cts: "2024-10-23T17:51:19.128Z", 135 + Exp: &expiresAt, 136 + Neg: &negated, 137 + Src: "did:plc:ewvi7nxzyoun6zhxrhs64oiz", 138 + Uri: "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.actor.profile/self", 139 + Val: "good", 140 + Ver: &version, 141 + Sig: []byte("sig"), // invalid, but we only care about the conversion 142 + } 143 + 144 + l := FromLexicon(lex) 145 + assert.Equal(lex.Ver, &l.Version) 146 + assert.Equal(lex.Cts, l.CreatedAt) 147 + assert.Equal(lex.Uri, l.URI) 148 + assert.Equal(lex.Val, l.Val) 149 + assert.Equal(lex.Cid, l.CID) 150 + assert.Equal(lex.Exp, l.ExpiresAt) 151 + assert.Equal(lex.Neg, l.Negated) 152 + assert.Equal(lex.Src, l.SourceDID) 153 + }
+2
automod/consumer/ozone.go
··· 60 60 oc.OzoneClient, 61 61 nil, // addedLabels []string 62 62 nil, // addedTags []string 63 + "", // ageAssuranceState 63 64 nil, // collections []string 64 65 "", // comment string 65 66 since.String(), // createdAfter string ··· 69 70 false, // hasComment bool 70 71 true, // includeAllUserRecords bool 71 72 limit, // limit int64 73 + nil, // modTool 72 74 nil, // policies []string 73 75 nil, // removedLabels []string 74 76 nil, // removedTags []string
+4
automod/engine/persisthelpers.go
··· 173 173 xrpcc, 174 174 nil, // addedLabels []string 175 175 nil, // addedTags []string 176 + "", // ageAssuranceState 176 177 nil, // collections []string 177 178 "", // comment string 178 179 "", // createdAfter string ··· 182 183 false, // hasComment bool 183 184 false, // includeAllUserRecords bool 184 185 5, // limit int64 186 + nil, // modTool 185 187 nil, // policies []string 186 188 nil, // removedLabels []string 187 189 nil, // removedTags []string ··· 255 257 xrpcc, 256 258 nil, // addedLabels []string 257 259 nil, // addedTags []string 260 + "", // ageAssuranceState 258 261 nil, // collections []string 259 262 "", // comment string 260 263 "", // createdAfter string ··· 264 267 false, // hasComment bool 265 268 false, // includeAllUserRecords bool 266 269 5, // limit int64 270 + nil, // modTool 267 271 nil, // policies []string 268 272 nil, // removedLabels []string 269 273 nil, // removedTags []string
+3 -1
carstore/repo_test.go
··· 16 16 appbsky "github.com/bluesky-social/indigo/api/bsky" 17 17 "github.com/bluesky-social/indigo/repo" 18 18 "github.com/bluesky-social/indigo/util" 19 - sqlbs "github.com/ipfs/go-bs-sqlite3" 19 + //sqlbs "github.com/ipfs/go-bs-sqlite3" 20 20 "github.com/ipfs/go-cid" 21 21 flatfs "github.com/ipfs/go-ds-flatfs" 22 22 blockstore "github.com/ipfs/go-ipfs-blockstore" ··· 452 452 } 453 453 } 454 454 455 + /* NOTE(bnewbold): this depends on github.com/ipfs/go-bs-sqlite3, which rewrote git history (?) breaking the dependency tree. We can roll forward, but that will require broad dependency updates. So for now just removing this benchmark/perf test. 455 456 func BenchmarkRepoWritesSqlite(b *testing.B) { 456 457 ctx := context.TODO() 457 458 ··· 489 490 head = nroot 490 491 } 491 492 } 493 + */ 492 494 493 495 func TestDuplicateBlockAcrossShards(ot *testing.T) { 494 496 ctx := context.TODO()
+2
cmd/beemo/notify_reports.go
··· 74 74 xrpcc, 75 75 nil, // addedLabels []string 76 76 nil, // addedTags []string 77 + "", // ageAssuranceState 77 78 nil, // collections []string 78 79 "", // comment string 79 80 "", // createdAfter string ··· 83 84 false, // hasComment bool 84 85 true, // includeAllUserRecords bool 85 86 limit, // limit int64 87 + nil, // modTool 86 88 nil, // policies []string 87 89 nil, // removedLabels []string 88 90 nil, // removedTags []string
+84 -1
cmd/goat/account.go
··· 8 8 "time" 9 9 10 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + "github.com/bluesky-social/indigo/atproto/auth" 12 + "github.com/bluesky-social/indigo/atproto/crypto" 11 13 "github.com/bluesky-social/indigo/atproto/syntax" 12 14 "github.com/bluesky-social/indigo/xrpc" 13 15 ··· 89 91 }, 90 92 &cli.Command{ 91 93 Name: "service-auth", 92 - Usage: "create service auth token", 94 + Usage: "ask the PDS to create a service auth token", 93 95 Flags: []cli.Flag{ 94 96 &cli.StringFlag{ 95 97 Name: "endpoint", ··· 109 111 }, 110 112 }, 111 113 Action: runAccountServiceAuth, 114 + }, 115 + &cli.Command{ 116 + Name: "service-auth-offline", 117 + Usage: "create service auth token via locally-held signing key", 118 + Flags: []cli.Flag{ 119 + &cli.StringFlag{ 120 + Name: "atproto-signing-key", 121 + Required: true, 122 + Usage: "private key used to sign the token (multibase syntax)", 123 + EnvVars: []string{"ATPROTO_SIGNING_KEY"}, 124 + }, 125 + &cli.StringFlag{ 126 + Name: "iss", 127 + Required: true, 128 + Usage: "the DID of the account issuing the token", 129 + }, 130 + &cli.StringFlag{ 131 + Name: "endpoint", 132 + Aliases: []string{"lxm"}, 133 + Usage: "restrict token to API endpoint (NSID, optional)", 134 + }, 135 + &cli.StringFlag{ 136 + Name: "audience", 137 + Aliases: []string{"aud"}, 138 + Required: true, 139 + Usage: "DID of service that will receive and validate token", 140 + }, 141 + &cli.IntFlag{ 142 + Name: "duration-sec", 143 + Value: 60, 144 + Usage: "validity time window of token (seconds)", 145 + }, 146 + }, 147 + Action: runAccountServiceAuthOffline, 112 148 }, 113 149 &cli.Command{ 114 150 Name: "create", ··· 365 401 } 366 402 367 403 fmt.Println(resp.Token) 404 + 405 + return nil 406 + } 407 + 408 + func runAccountServiceAuthOffline(cctx *cli.Context) error { 409 + privStr := cctx.String("atproto-signing-key") 410 + if privStr == "" { 411 + return fmt.Errorf("private key must be provided") 412 + } 413 + privkey, err := crypto.ParsePrivateMultibase(privStr) 414 + if err != nil { 415 + return fmt.Errorf("failed parsing private key: %w", err) 416 + } 417 + 418 + issString := cctx.String("iss") 419 + // TODO: support fragment identifiers 420 + iss, err := syntax.ParseDID(issString) 421 + if err != nil { 422 + return fmt.Errorf("iss argument must be a valid DID: %w", err) 423 + } 424 + 425 + lxmString := cctx.String("endpoint") 426 + var lxm *syntax.NSID = nil 427 + if lxmString != "" { 428 + lxmTmp, err := syntax.ParseNSID(lxmString) 429 + if err != nil { 430 + return fmt.Errorf("lxm argument must be a valid NSID: %w", err) 431 + } 432 + lxm = &lxmTmp 433 + } 434 + 435 + aud := cctx.String("audience") 436 + // TODO: can aud DID have a fragment? 437 + _, err = syntax.ParseDID(aud) 438 + if err != nil { 439 + return fmt.Errorf("aud argument must be a valid DID: %w", err) 440 + } 441 + 442 + durSec := cctx.Int("duration-sec") 443 + duration := time.Duration(durSec * int(time.Second)) 444 + 445 + token, err := auth.SignServiceAuth(iss, aud, duration, lxm, privkey) 446 + if err != nil { 447 + return fmt.Errorf("failed signing token: %w", err) 448 + } 449 + 450 + fmt.Println(token) 368 451 369 452 return nil 370 453 }
+441
cmd/goat/plc.go
··· 6 6 "fmt" 7 7 "io" 8 8 "net/http" 9 + "net/url" 9 10 "strings" 10 11 "time" 11 12 13 + "github.com/bluesky-social/indigo/atproto/crypto" 12 14 "github.com/bluesky-social/indigo/atproto/identity" 13 15 "github.com/bluesky-social/indigo/atproto/syntax" 14 16 "github.com/bluesky-social/indigo/util" 17 + 18 + "github.com/did-method-plc/go-didplc" 15 19 16 20 "github.com/urfave/cli/v2" 17 21 ) ··· 70 74 }, 71 75 }, 72 76 Action: runPLCDump, 77 + }, 78 + &cli.Command{ 79 + Name: "genesis", 80 + Usage: "produce an unsigned genesis operation", 81 + Flags: []cli.Flag{ 82 + &cli.StringFlag{ 83 + Name: "handle", 84 + Usage: "atproto handle", 85 + }, 86 + &cli.StringSliceFlag{ 87 + Name: "rotation-key", 88 + Usage: "rotation public key, in did:key format", 89 + }, 90 + &cli.StringFlag{ 91 + Name: "atproto-key", 92 + Usage: "atproto repo signing public key, in did:key format", 93 + }, 94 + &cli.StringFlag{ 95 + Name: "pds", 96 + Usage: "atproto PDS service URL", 97 + }, 98 + }, 99 + Action: runPLCGenesis, 100 + }, 101 + &cli.Command{ 102 + Name: "calc-did", 103 + Usage: "calculate the DID corresponding to a signed PLC operation", 104 + ArgsUsage: `<signed_genesis.json>`, 105 + Flags: []cli.Flag{}, 106 + Action: runPLCCalcDID, 107 + }, 108 + &cli.Command{ 109 + Name: "sign", 110 + Usage: "sign an operation, ready to be submitted", 111 + ArgsUsage: `<operation.json>`, 112 + Flags: []cli.Flag{ 113 + &cli.StringFlag{ 114 + Name: "plc-signing-key", 115 + Usage: "private key used to sign operation (multibase syntax)", 116 + EnvVars: []string{"PLC_SIGNING_KEY"}, 117 + }, 118 + }, 119 + Action: runPLCSign, 120 + }, 121 + &cli.Command{ 122 + Name: "submit", 123 + Usage: "submit a signed operation to the PLC directory", 124 + ArgsUsage: `<signed_operation.json>`, 125 + Flags: []cli.Flag{ 126 + &cli.BoolFlag{ 127 + Name: "genesis", 128 + Usage: "the operation is a genesis operation", 129 + }, 130 + &cli.StringFlag{ 131 + Name: "did", 132 + Usage: "the DID of the identity to update", 133 + }, 134 + }, 135 + Action: runPLCSubmit, 136 + }, 137 + &cli.Command{ 138 + Name: "update", 139 + Usage: "apply updates to a previous operation to produce a new one (but don't sign or submit it, yet)", 140 + ArgsUsage: `<DID>`, 141 + Flags: []cli.Flag{ 142 + &cli.StringFlag{ 143 + Name: "prev", 144 + Usage: "the CID of the operation to use as a base (uses most recent op if not specified)", 145 + }, 146 + &cli.StringFlag{ 147 + Name: "handle", 148 + Usage: "atproto handle", 149 + }, 150 + &cli.StringSliceFlag{ 151 + Name: "add-rotation-key", 152 + Usage: "rotation public key, in did:key format (added to front of rotationKey list)", 153 + }, 154 + &cli.StringSliceFlag{ 155 + Name: "remove-rotation-key", 156 + Usage: "rotation public key, in did:key format", 157 + }, 158 + &cli.StringFlag{ 159 + Name: "atproto-key", 160 + Usage: "atproto repo signing public key, in did:key format", 161 + }, 162 + &cli.StringFlag{ 163 + Name: "pds", 164 + Usage: "atproto PDS service URL", 165 + }, 166 + }, 167 + Action: runPLCUpdate, 73 168 }, 74 169 }, 75 170 } ··· 320 415 } 321 416 return &d, nil 322 417 } 418 + 419 + func runPLCGenesis(cctx *cli.Context) error { 420 + // TODO: helper function in didplc to make an empty op like this? 421 + services := make(map[string]didplc.OpService) 422 + verifMethods := make(map[string]string) 423 + op := didplc.RegularOp{ 424 + Type: "plc_operation", 425 + RotationKeys: []string{}, 426 + VerificationMethods: verifMethods, 427 + AlsoKnownAs: []string{}, 428 + Services: services, 429 + } 430 + 431 + for _, rotationKey := range cctx.StringSlice("rotation-key") { 432 + if _, err := crypto.ParsePublicDIDKey(rotationKey); err != nil { 433 + return err 434 + } 435 + op.RotationKeys = append(op.RotationKeys, rotationKey) 436 + } 437 + 438 + handle := cctx.String("handle") 439 + if handle != "" { 440 + parsedHandle, err := syntax.ParseHandle(strings.TrimPrefix(handle, "at://")) 441 + if err != nil { 442 + return err 443 + } 444 + parsedHandle = parsedHandle.Normalize() 445 + op.AlsoKnownAs = append(op.AlsoKnownAs, "at://"+string(parsedHandle)) 446 + } 447 + 448 + atprotoKey := cctx.String("atproto-key") 449 + if atprotoKey != "" { 450 + if _, err := crypto.ParsePublicDIDKey(atprotoKey); err != nil { 451 + return err 452 + } 453 + op.VerificationMethods["atproto"] = atprotoKey 454 + } 455 + 456 + pds := cctx.String("pds") 457 + if pds != "" { 458 + parsedUrl, err := url.Parse(pds) 459 + if err != nil { 460 + return err 461 + } 462 + if !parsedUrl.IsAbs() { 463 + return fmt.Errorf("invalid PDS URL: must be absolute") 464 + } 465 + op.Services["atproto_pds"] = didplc.OpService{ 466 + Type: "AtprotoPersonalDataServer", 467 + Endpoint: pds, 468 + } 469 + } 470 + 471 + res, err := json.MarshalIndent(op, "", " ") 472 + if err != nil { 473 + return err 474 + } 475 + fmt.Println(string(res)) 476 + 477 + return nil 478 + } 479 + 480 + func runPLCCalcDID(cctx *cli.Context) error { 481 + s := cctx.Args().First() 482 + if s == "" { 483 + return fmt.Errorf("need to provide genesis json path as input") 484 + } 485 + 486 + inputReader, err := getFileOrStdin(s) 487 + if err != nil { 488 + return err 489 + } 490 + 491 + inBytes, err := io.ReadAll(inputReader) 492 + if err != nil { 493 + return err 494 + } 495 + 496 + var enum didplc.OpEnum 497 + if err := json.Unmarshal(inBytes, &enum); err != nil { 498 + return err 499 + } 500 + op := enum.AsOperation() 501 + 502 + did, err := op.DID() // errors if op is not a signed genesis op 503 + if err != nil { 504 + return err 505 + } 506 + 507 + fmt.Println(did) 508 + 509 + return nil 510 + } 511 + 512 + func runPLCSign(cctx *cli.Context) error { 513 + s := cctx.Args().First() 514 + if s == "" { 515 + return fmt.Errorf("need to provide PLC operation json path as input") 516 + } 517 + 518 + privStr := cctx.String("plc-signing-key") 519 + if privStr == "" { 520 + return fmt.Errorf("private key must be provided") 521 + } 522 + 523 + inputReader, err := getFileOrStdin(s) 524 + if err != nil { 525 + return err 526 + } 527 + 528 + inBytes, err := io.ReadAll(inputReader) 529 + if err != nil { 530 + return err 531 + } 532 + 533 + var enum didplc.OpEnum 534 + if err := json.Unmarshal(inBytes, &enum); err != nil { 535 + return err 536 + } 537 + op := enum.AsOperation() 538 + 539 + // Note: we do not require that the op is currently unsigned. 540 + // If it's already signed, we'll re-sign it. 541 + 542 + privkey, err := crypto.ParsePrivateMultibase(privStr) 543 + if err != nil { 544 + return err 545 + } 546 + 547 + if err := op.Sign(privkey); err != nil { 548 + return err 549 + } 550 + 551 + res, err := json.MarshalIndent(op, "", " ") 552 + if err != nil { 553 + return err 554 + } 555 + fmt.Println(string(res)) 556 + 557 + return nil 558 + } 559 + 560 + func runPLCSubmit(cctx *cli.Context) error { 561 + ctx := context.Background() 562 + expectGenesis := cctx.Bool("genesis") 563 + didString := cctx.String("did") 564 + 565 + if !expectGenesis && didString == "" { 566 + return fmt.Errorf("exactly one of either --genesis or --did must be specified") 567 + } 568 + 569 + if expectGenesis && didString != "" { 570 + return fmt.Errorf("exactly one of either --genesis or --did must be specified") 571 + } 572 + 573 + s := cctx.Args().First() 574 + if s == "" { 575 + return fmt.Errorf("need to provide PLC operation json path as input") 576 + } 577 + 578 + inputReader, err := getFileOrStdin(s) 579 + if err != nil { 580 + return err 581 + } 582 + 583 + inBytes, err := io.ReadAll(inputReader) 584 + if err != nil { 585 + return err 586 + } 587 + 588 + var enum didplc.OpEnum 589 + if err := json.Unmarshal(inBytes, &enum); err != nil { 590 + return err 591 + } 592 + op := enum.AsOperation() 593 + 594 + if op.IsGenesis() != expectGenesis { 595 + if expectGenesis { 596 + return fmt.Errorf("expected genesis operation, but a non-genesis operation was provided") 597 + } else { 598 + return fmt.Errorf("expected non-genesis operation, but a genesis operation was provided") 599 + } 600 + } 601 + 602 + if op.IsGenesis() { 603 + didString, err = op.DID() 604 + if err != nil { 605 + return err 606 + } 607 + } 608 + 609 + if !op.IsSigned() { 610 + return fmt.Errorf("operation must be signed") 611 + } 612 + 613 + c := didplc.Client{ 614 + DirectoryURL: cctx.String("plc-host"), 615 + UserAgent: *userAgent(), 616 + } 617 + 618 + if err = c.Submit(ctx, didString, op); err != nil { 619 + return err 620 + } 621 + 622 + fmt.Println("success") 623 + 624 + return nil 625 + } 626 + 627 + // fetch logs from /log/audit, select according to base_cid ("" means use latest), and 628 + // prepare it for updates: 629 + // - convert from legacy op format if needed (and reject tombstone ops) 630 + // - strip signature 631 + // - set `prev` to appropriate value 632 + func fetchOpForUpdate(ctx context.Context, c didplc.Client, did string, base_cid string) (*didplc.RegularOp, error) { 633 + auditlog, err := c.AuditLog(ctx, did) 634 + if err != nil { 635 + return nil, err 636 + } 637 + 638 + if err = didplc.VerifyOpLog(auditlog); err != nil { 639 + return nil, err 640 + } 641 + 642 + var baseLogEntry *didplc.LogEntry 643 + if base_cid == "" { 644 + // use most recent entry 645 + baseLogEntry = &auditlog[len(auditlog)-1] 646 + } else { 647 + // scan for the specified entry 648 + for _, entry := range auditlog { 649 + if entry.CID == base_cid { 650 + baseLogEntry = &entry 651 + break 652 + } 653 + } 654 + if baseLogEntry == nil { 655 + return nil, fmt.Errorf("no operation found matching CID %s", base_cid) 656 + } 657 + } 658 + var op didplc.RegularOp 659 + switch baseOp := baseLogEntry.Operation.AsOperation().(type) { 660 + case *didplc.RegularOp: 661 + op = *baseOp 662 + op.Sig = nil 663 + case *didplc.LegacyOp: 664 + op = baseOp.RegularOp() // also strips sig 665 + case *didplc.TombstoneOp: 666 + return nil, fmt.Errorf("cannot update from a tombstone op") 667 + } 668 + op.Prev = &baseLogEntry.CID 669 + return &op, nil 670 + } 671 + 672 + func runPLCUpdate(cctx *cli.Context) error { 673 + ctx := context.Background() 674 + prevCID := cctx.String("prev") 675 + 676 + didString := cctx.Args().First() 677 + if didString == "" { 678 + return fmt.Errorf("please specify a DID to update") 679 + } 680 + 681 + c := didplc.Client{ 682 + DirectoryURL: cctx.String("plc-host"), 683 + UserAgent: *userAgent(), 684 + } 685 + op, err := fetchOpForUpdate(ctx, c, didString, prevCID) 686 + if err != nil { 687 + return err 688 + } 689 + 690 + for _, rotationKey := range cctx.StringSlice("remove-rotation-key") { 691 + if _, err := crypto.ParsePublicDIDKey(rotationKey); err != nil { 692 + return err 693 + } 694 + removeSuccess := false 695 + for idx, existingRotationKey := range op.RotationKeys { 696 + if existingRotationKey == rotationKey { 697 + op.RotationKeys = append(op.RotationKeys[:idx], op.RotationKeys[idx+1:]...) 698 + removeSuccess = true 699 + } 700 + } 701 + if !removeSuccess { 702 + return fmt.Errorf("failed remove rotation key %s, not found in array", rotationKey) 703 + } 704 + } 705 + 706 + for _, rotationKey := range cctx.StringSlice("add-rotation-key") { 707 + if _, err := crypto.ParsePublicDIDKey(rotationKey); err != nil { 708 + return err 709 + } 710 + // prepend (Note: if adding multiple rotation keys at once, they'll end up in reverse order) 711 + op.RotationKeys = append([]string{rotationKey}, op.RotationKeys...) 712 + } 713 + 714 + handle := cctx.String("handle") 715 + if handle != "" { 716 + parsedHandle, err := syntax.ParseHandle(strings.TrimPrefix(handle, "at://")) 717 + if err != nil { 718 + return err 719 + } 720 + 721 + // strip any existing at:// akas 722 + // (someone might have some non-atproto akas, we will leave them untouched, 723 + // they can manually manage those or use some other tool if needed) 724 + var akas []string 725 + for _, aka := range op.AlsoKnownAs { 726 + if !strings.HasPrefix(aka, "at://") { 727 + akas = append(akas, aka) 728 + } 729 + } 730 + op.AlsoKnownAs = append(akas, "at://"+string(parsedHandle)) 731 + } 732 + 733 + atprotoKey := cctx.String("atproto-key") 734 + if atprotoKey != "" { 735 + if _, err := crypto.ParsePublicDIDKey(atprotoKey); err != nil { 736 + return err 737 + } 738 + op.VerificationMethods["atproto"] = atprotoKey 739 + } 740 + 741 + pds := cctx.String("pds") 742 + if pds != "" { 743 + parsedUrl, err := url.Parse(pds) 744 + if err != nil { 745 + return err 746 + } 747 + if !parsedUrl.IsAbs() { 748 + return fmt.Errorf("invalid PDS URL: must be absolute") 749 + } 750 + op.Services["atproto_pds"] = didplc.OpService{ 751 + Type: "AtprotoPersonalDataServer", 752 + Endpoint: pds, 753 + } 754 + } 755 + 756 + res, err := json.MarshalIndent(op, "", " ") 757 + if err != nil { 758 + return err 759 + } 760 + fmt.Println(string(res)) 761 + 762 + return nil 763 + }
+4
cmd/gosky/admin.go
··· 394 394 xrpcc, 395 395 nil, // addedLabels []string 396 396 nil, // addedTags []string 397 + "", // ageAssuranceState 397 398 nil, // collections []string 398 399 "", // comment string 399 400 "", // createdAfter string ··· 403 404 false, // hasComment bool 404 405 false, // includeAllUserRecords bool 405 406 100, // limit int64 407 + nil, // modTool 406 408 nil, // policies []string 407 409 nil, // removedLabels []string 408 410 nil, // removedTags []string ··· 709 711 xrpcc, 710 712 nil, // addedLabels []string 711 713 nil, // addedTags []string 714 + "", // ageAssuranceState 712 715 nil, // collections []string 713 716 "", // comment string 714 717 "", // createdAfter string ··· 718 721 false, // hasComment bool 719 722 false, // includeAllUserRecords bool 720 723 100, // limit int64 724 + nil, // modTool 721 725 nil, // policies []string 722 726 nil, // removedLabels []string 723 727 nil, // removedTags []string
-26
docs/auth.md
··· 1 - # Auth 2 - 3 - The auth system uses two tokens, an access token and a refresh token. 4 - 5 - The access token is a jwt with the following values: 6 - ``` 7 - scope: "com.atproto.access" 8 - sub: <the users DID> 9 - iat: the current time, in unix epoch seconds 10 - exp: the expiry date, usually around an hour, but at least 15 minutes 11 - ``` 12 - 13 - The refresh token is a jwt with the following values: 14 - ``` 15 - scope: "com.atproto.refresh" 16 - sub: <the users DID> 17 - iat: the current time, in unix epoch seconds 18 - exp: the expiry date, usually around a week, must be significantly longer than the access token 19 - jti: a unique identifier for this token 20 - ``` 21 - 22 - The access token is what is used for all requests, however since it expires 23 - quickly, it must be refreshed periodically using the refresh token. 24 - When the refresh token is used, it must be marked as deleted, and the new token then replaces it. 25 - Note: The old access token is not necessarily disabled at that point of refreshing. 26 -
-37
docs/feed-proposal.md
··· 1 - # Feed Structuring Proposal 2 - 3 - Some thoughts on a new format for feeds. 4 - 5 - ## Motivation 6 - The interface for requesting and getting back feeds is something that I feel is really at the core of what bluesky offers. The user should be able to choose what feeds they subscribe to, feeds should be first class objects, they should be able to be efficiently generated and consumed, and they should be able to trustlessly come from anywhere. 7 - There are a lot of changes we *could* make to the current structure, but I don't want to stray too far from where we are at right now. 8 - 9 - 10 - ```go 11 - type Feed struct { 12 - Items []FeedItem 13 - Values map[Cid]Record 14 - ItemInfos map[Uri]ItemInfo 15 - ActorInfos map[Did]ActorInfo 16 - } 17 - 18 - type FeedItem struct { 19 - Uri string 20 - Replies []Uri 21 - ReplyTo Uri 22 - RepostedBy Did 23 - } 24 - 25 - type ItemInfo struct { 26 - Cid Cid 27 - Upvotes int 28 - Reposts int 29 - Replies int 30 - Author Did 31 - } 32 - ``` 33 - 34 - The main idea here is not repeating ourselves, while still providing all the information the client might need. 35 - With this structure too, the user could easily request *less* data, asking to 36 - skip the inclusion of records older than X, or saying they are okay with stale 37 - information in certain places for the sake of efficiency.
+1 -1
go.mod
··· 13 13 github.com/brianvoe/gofakeit/v6 v6.25.0 14 14 github.com/carlmjohnson/versioninfo v0.22.5 15 15 github.com/cockroachdb/pebble v1.1.2 16 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038 16 17 github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 17 18 github.com/flosch/pongo2/v6 v6.0.0 18 19 github.com/go-redis/cache/v9 v9.0.0 ··· 25 26 github.com/hashicorp/golang-lru/v2 v2.0.7 26 27 github.com/icrowley/fake v0.0.0-20221112152111-d7b7e2276db2 27 28 github.com/ipfs/go-block-format v0.2.0 28 - github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d 29 29 github.com/ipfs/go-cid v0.4.1 30 30 github.com/ipfs/go-datastore v0.6.0 31 31 github.com/ipfs/go-ds-flatfs v0.5.1
+2 -2
go.sum
··· 81 81 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 82 82 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 83 83 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 84 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038 h1:AGh+Vn9fXhf9eo8erG1CK4+LACduPo64P1OICQLDv88= 85 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038/go.mod h1:ddIXqTTSXWtj5kMsHAPj8SvbIx2GZdAkBFgFa6e6+CM= 84 86 github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE= 85 87 github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= 86 88 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= ··· 184 186 github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 185 187 github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= 186 188 github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= 187 - github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d h1:9V+GGXCuOfDiFpdAHz58q9mKLg447xp0cQKvqQrAwYE= 188 - github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d/go.mod h1:pMbnFyNAGjryYCLCe59YDLRv/ujdN+zGJBT1umlvYRM= 189 189 github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 190 190 github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 191 191 github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk=
+1 -1
mst/mst_test.go
··· 532 532 for i := 0; i < 256; i++ { 533 533 f.Add([]byte{byte(i)}) 534 534 } 535 - rx := regexp.MustCompile("^[a-zA-Z0-9_:.-]+$") 535 + rx := regexp.MustCompile("^[a-zA-Z0-9_:.~-]+$") 536 536 f.Fuzz(func(t *testing.T, in []byte) { 537 537 s := string(in) 538 538 if a, b := rx.MatchString(s), keyHasAllValidChars(s); a != b {
+2 -2
mst/mst_util.go
··· 197 197 } 198 198 199 199 // keyHasAllValidChars reports whether s matches 200 - // the regexp /^[a-zA-Z0-9_:.-]+$/ without using regexp, 200 + // the regexp /^[a-zA-Z0-9_:.~-]+$/ without using regexp, 201 201 // which is slower. 202 202 func keyHasAllValidChars(s string) bool { 203 203 if len(s) == 0 { ··· 211 211 continue 212 212 } 213 213 switch b { 214 - case '_', ':', '.', '-': 214 + case '_', ':', '.', '~', '-': 215 215 continue 216 216 default: 217 217 return false
+135
pkg/robusthttp/client.go
··· 1 + package robusthttp 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "net/http" 7 + "time" 8 + 9 + "github.com/hashicorp/go-cleanhttp" 10 + "github.com/hashicorp/go-retryablehttp" 11 + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 12 + ) 13 + 14 + type LeveledSlog struct { 15 + inner *slog.Logger 16 + } 17 + 18 + // re-writes HTTP client ERROR to WARN level (because of retries) 19 + func (l LeveledSlog) Error(msg string, keysAndValues ...any) { 20 + l.inner.Warn(msg, keysAndValues...) 21 + } 22 + 23 + func (l LeveledSlog) Warn(msg string, keysAndValues ...any) { 24 + l.inner.Warn(msg, keysAndValues...) 25 + } 26 + 27 + func (l LeveledSlog) Info(msg string, keysAndValues ...any) { 28 + l.inner.Info(msg, keysAndValues...) 29 + } 30 + 31 + func (l LeveledSlog) Debug(msg string, keysAndValues ...any) { 32 + l.inner.Debug(msg, keysAndValues...) 33 + } 34 + 35 + type Option func(*retryablehttp.Client) 36 + 37 + // WithMaxRetries sets the maximum number of retries for the HTTP client. 38 + func WithMaxRetries(maxRetries int) Option { 39 + return func(client *retryablehttp.Client) { 40 + client.RetryMax = maxRetries 41 + } 42 + } 43 + 44 + // WithRetryWaitMin sets the minimum wait time between retries. 45 + func WithRetryWaitMin(waitMin time.Duration) Option { 46 + return func(client *retryablehttp.Client) { 47 + client.RetryWaitMin = waitMin 48 + } 49 + } 50 + 51 + // WithRetryWaitMax sets the maximum wait time between retries. 52 + func WithRetryWaitMax(waitMax time.Duration) Option { 53 + return func(client *retryablehttp.Client) { 54 + client.RetryWaitMax = waitMax 55 + } 56 + } 57 + 58 + // WithLogger sets a custom logger for the HTTP client. 59 + func WithLogger(logger *slog.Logger) Option { 60 + return func(client *retryablehttp.Client) { 61 + client.Logger = retryablehttp.LeveledLogger(LeveledSlog{inner: logger}) 62 + } 63 + } 64 + 65 + // WithTransport sets a custom transport for the HTTP client. 66 + func WithTransport(transport http.RoundTripper) Option { 67 + return func(client *retryablehttp.Client) { 68 + client.HTTPClient.Transport = transport 69 + } 70 + } 71 + 72 + // WithRetryPolicy sets a custom retry policy for the HTTP client. 73 + func WithRetryPolicy(policy retryablehttp.CheckRetry) Option { 74 + return func(client *retryablehttp.Client) { 75 + client.CheckRetry = policy 76 + } 77 + } 78 + 79 + // Generates an HTTP client with decent general-purpose defaults around 80 + // timeouts and retries. The returned client has the stdlib http.Client 81 + // interface, but has Hashicorp retryablehttp logic internally. 82 + // 83 + // This client will retry on connection errors, 5xx status (except 501). 84 + // It will log intermediate failures with WARN level. This does not start from 85 + // http.DefaultClient. 86 + // 87 + // This should be usable for XRPC clients, and other general inter-service 88 + // client needs. CLI tools might want shorter timeouts and fewer retries by 89 + // default. 90 + func NewClient(options ...Option) *http.Client { 91 + logger := LeveledSlog{inner: slog.Default().With("subsystem", "RobustHTTPClient")} 92 + retryClient := retryablehttp.NewClient() 93 + retryClient.HTTPClient.Transport = otelhttp.NewTransport(cleanhttp.DefaultPooledTransport()) 94 + retryClient.RetryMax = 3 95 + retryClient.RetryWaitMin = 1 * time.Second 96 + retryClient.RetryWaitMax = 10 * time.Second 97 + retryClient.Logger = retryablehttp.LeveledLogger(logger) 98 + retryClient.CheckRetry = DefaultRetryPolicy 99 + 100 + for _, option := range options { 101 + option(retryClient) 102 + } 103 + 104 + client := retryClient.StandardClient() 105 + client.Timeout = 30 * time.Second 106 + return client 107 + } 108 + 109 + // For use in local integration tests. Short timeouts, no retries, etc 110 + func TestingHTTPClient() *http.Client { 111 + 112 + client := http.DefaultClient 113 + client.Timeout = 1 * time.Second 114 + return client 115 + } 116 + 117 + // DefaultRetryPolicy is a custom wrapper around retryablehttp.DefaultRetryPolicy. 118 + // It treats `429 Too Many Requests` as non-retryable, so the application can decide 119 + // how to deal with rate-limiting. 120 + func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) { 121 + if err == nil && resp.StatusCode == http.StatusTooManyRequests { 122 + return false, nil 123 + } 124 + return retryablehttp.DefaultRetryPolicy(ctx, resp, err) 125 + } 126 + 127 + func NoInternalServerErrorPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) { 128 + if err == nil && resp.StatusCode == http.StatusTooManyRequests { 129 + return false, nil 130 + } 131 + if err == nil && resp.StatusCode == http.StatusInternalServerError { 132 + return false, nil 133 + } 134 + return retryablehttp.DefaultRetryPolicy(ctx, resp, err) 135 + }