forges 0.0.0 → 0.1.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.
- package/LICENCE +21 -0
- package/README.md +72 -0
- package/dist/azure-devops/index.d.mts +2 -0
- package/dist/azure-devops/index.mjs +1 -0
- package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
- package/dist/bitbucket/index.d.mts +2 -0
- package/dist/bitbucket/index.mjs +1 -0
- package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
- package/dist/crypto-CQNEyYHD.mjs +156 -0
- package/dist/cursor-origin/index.d.mts +2 -0
- package/dist/cursor-origin/index.mjs +1 -0
- package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
- package/dist/define-B2_4ob2i.d.mts +307 -0
- package/dist/define-D3yEXtKK.mjs +2276 -0
- package/dist/env.d.mts +46 -0
- package/dist/env.mjs +283 -0
- package/dist/fake/index.d.mts +227 -0
- package/dist/fake/index.mjs +1001 -0
- package/dist/fetch-D4pSRvvA.d.mts +1723 -0
- package/dist/forgejo/index.d.mts +2 -0
- package/dist/forgejo/index.mjs +1 -0
- package/dist/forgejo-BCefbHUN.mjs +2005 -0
- package/dist/gitea/index.d.mts +2 -0
- package/dist/gitea/index.mjs +22 -0
- package/dist/gitee/index.d.mts +2 -0
- package/dist/gitee/index.mjs +1 -0
- package/dist/gitee-CA-f7Rs8.mjs +1272 -0
- package/dist/github/index.d.mts +2 -0
- package/dist/github/index.mjs +1 -0
- package/dist/github-B0umL8_f.mjs +3259 -0
- package/dist/gitlab/index.d.mts +2 -0
- package/dist/gitlab/index.mjs +1 -0
- package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
- package/dist/graphql-D7uGzZTf.mjs +366 -0
- package/dist/index-BQ6VS31s.d.mts +32 -0
- package/dist/index-C-jz9Mo3.d.mts +16 -0
- package/dist/index-C4fYzmK3.d.mts +102 -0
- package/dist/index-CQiIX4SH.d.mts +27 -0
- package/dist/index-CTj5gQzl.d.mts +35 -0
- package/dist/index-CliBWzOF.d.mts +23 -0
- package/dist/index-Cz_y-t3d.d.mts +36 -0
- package/dist/index-DEt25E1w.d.mts +77 -0
- package/dist/index-DIuS0EOy.d.mts +43 -0
- package/dist/index-uSpsMCrC.d.mts +26 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.mjs +13 -0
- package/dist/kit.d.mts +183 -0
- package/dist/kit.mjs +4 -0
- package/dist/provider-CoZ7cCxp.d.mts +1439 -0
- package/dist/provider-Ed1YhkQN.mjs +169 -0
- package/dist/pushin/index.d.mts +2 -0
- package/dist/pushin/index.mjs +1 -0
- package/dist/pushin-CkxgJ2cU.mjs +343 -0
- package/dist/schema/Actor.json +96 -0
- package/dist/schema/AlertSupport.json +49 -0
- package/dist/schema/AuthKind.json +15 -0
- package/dist/schema/Branch.json +31 -0
- package/dist/schema/ChangedFile.json +58 -0
- package/dist/schema/Check.json +169 -0
- package/dist/schema/CheckRef.json +114 -0
- package/dist/schema/CheckState.json +14 -0
- package/dist/schema/ChecksSummary.json +34 -0
- package/dist/schema/CiJob.json +186 -0
- package/dist/schema/CiJobRef.json +132 -0
- package/dist/schema/CiRun.json +234 -0
- package/dist/schema/CiRunRef.json +103 -0
- package/dist/schema/CodeFragment.json +19 -0
- package/dist/schema/CodeMatch.json +128 -0
- package/dist/schema/Collaborator.json +169 -0
- package/dist/schema/Comment.json +336 -0
- package/dist/schema/CommentRef.json +158 -0
- package/dist/schema/Commit.json +307 -0
- package/dist/schema/CommitRef.json +104 -0
- package/dist/schema/CommitSignature.json +118 -0
- package/dist/schema/Comparison.json +355 -0
- package/dist/schema/Cursor.json +21 -0
- package/dist/schema/EventAction.json +33 -0
- package/dist/schema/EventDetail.json +649 -0
- package/dist/schema/EventKind.json +26 -0
- package/dist/schema/FileContent.json +73 -0
- package/dist/schema/FileStatus.json +15 -0
- package/dist/schema/ForgeCapabilities.json +737 -0
- package/dist/schema/ForgeEvent.json +789 -0
- package/dist/schema/ForgeInstance.json +7 -0
- package/dist/schema/ForgeKind.json +32 -0
- package/dist/schema/ForgeWarning.json +49 -0
- package/dist/schema/ForgeWarningCode.json +7 -0
- package/dist/schema/GetManyResult.json +699 -0
- package/dist/schema/Installation.json +131 -0
- package/dist/schema/InstallationToken.json +31 -0
- package/dist/schema/KnownForgeKind.json +19 -0
- package/dist/schema/Label.json +22 -0
- package/dist/schema/MergeMethod.json +14 -0
- package/dist/schema/Milestone.json +40 -0
- package/dist/schema/Notification.json +498 -0
- package/dist/schema/NotificationReason.json +28 -0
- package/dist/schema/NotificationRef.json +60 -0
- package/dist/schema/NotificationSubject.json +376 -0
- package/dist/schema/PerKind.json +49 -0
- package/dist/schema/PullBranches.json +125 -0
- package/dist/schema/PushCommit.json +24 -0
- package/dist/schema/Reaction.json +148 -0
- package/dist/schema/ReactionContent.json +17 -0
- package/dist/schema/ReactionSummary.json +70 -0
- package/dist/schema/Release.json +286 -0
- package/dist/schema/ReleaseAsset.json +169 -0
- package/dist/schema/ReleaseAssetRef.json +138 -0
- package/dist/schema/ReleaseRef.json +107 -0
- package/dist/schema/Repo.json +354 -0
- package/dist/schema/RepoFeatures.json +35 -0
- package/dist/schema/RepoPermissions.json +35 -0
- package/dist/schema/RepoRef.json +77 -0
- package/dist/schema/RepoRole.json +15 -0
- package/dist/schema/RepoVisibility.json +12 -0
- package/dist/schema/Review.json +371 -0
- package/dist/schema/ReviewComment.json +280 -0
- package/dist/schema/ReviewCommentInput.json +34 -0
- package/dist/schema/ReviewRef.json +158 -0
- package/dist/schema/ReviewState.json +15 -0
- package/dist/schema/Reviewer.json +133 -0
- package/dist/schema/SecurityAlert.json +219 -0
- package/dist/schema/SecurityAlertKind.json +14 -0
- package/dist/schema/SecurityAlertRef.json +122 -0
- package/dist/schema/SecuritySeverity.json +14 -0
- package/dist/schema/SourceKind.json +12 -0
- package/dist/schema/SubscriptionState.json +12 -0
- package/dist/schema/Support.json +23 -0
- package/dist/schema/Tag.json +25 -0
- package/dist/schema/TextLimits.json +18 -0
- package/dist/schema/Thread.json +653 -0
- package/dist/schema/ThreadKind.json +14 -0
- package/dist/schema/ThreadRef.json +131 -0
- package/dist/schema/ThreadStack.json +146 -0
- package/dist/schema/ThreadState.json +13 -0
- package/dist/schema/TreeEntry.json +46 -0
- package/dist/schema/TreeEntryType.json +13 -0
- package/dist/schema/UpsertCommentResult.json +355 -0
- package/dist/schema/User.json +123 -0
- package/dist/schema/Webhook.json +188 -0
- package/dist/schema/WebhookDeliveryRecord.json +173 -0
- package/dist/schema/WebhookDeliveryRef.json +135 -0
- package/dist/schema/WebhookRef.json +105 -0
- package/dist/schema/index.d.mts +4 -0
- package/dist/schema/index.mjs +3219 -0
- package/dist/tangled/index.d.mts +2 -0
- package/dist/tangled/index.mjs +1 -0
- package/dist/tangled-ChdPmiv-.mjs +1570 -0
- package/dist/testing/index.d.mts +123 -0
- package/dist/testing/index.mjs +242 -0
- package/dist/webhooks-rn9-8lxT.mjs +70 -0
- package/package.json +115 -3
- package/dist/.gitkeep +0 -0
|
@@ -0,0 +1,1570 @@
|
|
|
1
|
+
import { At as UnresolvedThreadError, Dt as SubscriptionClosedError, Mt as WebhookVerificationError, O as toDate, Ot as TokenRevokedError, S as phased, Tt as RateLimitedError, _ as mapConcurrent, _t as ForgeError, bt as InsufficientScopeError, ct as eventAction, d as forgeIterable, f as getManyConcurrently, gt as ForgeApiError, j as toWarning, jt as UnsupportedOperationError, n as perKind, pt as AuthenticationRequiredError, r as verb, rt as createFetcher, t as defineForgeProvider, tt as abortable, u as degradesToWarning, w as requireThread, wt as NotFoundError } from "./define-D3yEXtKK.mjs";
|
|
2
|
+
import { n as headerValue, t as bodyText } from "./crypto-CQNEyYHD.mjs";
|
|
3
|
+
import { a as verifyHmacSignature, i as refEvent } from "./webhooks-rn9-8lxT.mjs";
|
|
4
|
+
//#region src/tangled/atproto.ts
|
|
5
|
+
function parseAtUri(uri) {
|
|
6
|
+
const match = /^at:\/\/(did:[^/]+)\/([^/]+)\/([^/?#]+)$/.exec(uri);
|
|
7
|
+
return match ? {
|
|
8
|
+
did: match[1],
|
|
9
|
+
collection: match[2],
|
|
10
|
+
rkey: match[3]
|
|
11
|
+
} : void 0;
|
|
12
|
+
}
|
|
13
|
+
function atUri(did, collection, rkey) {
|
|
14
|
+
return `at://${did}/${collection}/${rkey}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolves identities and reads records straight from each author's PDS.
|
|
18
|
+
* Backlinks come from a Constellation-compatible index, since no PDS can
|
|
19
|
+
* list the records that point at a given one.
|
|
20
|
+
*/
|
|
21
|
+
function createAtprotoClient(options) {
|
|
22
|
+
const identities = /* @__PURE__ */ new Map();
|
|
23
|
+
function resolveDid(did, signal) {
|
|
24
|
+
let identity = identities.get(did);
|
|
25
|
+
if (!identity) {
|
|
26
|
+
const url = did.startsWith("did:web:") ? `https://${decodeURIComponent(did.slice(8))}/.well-known/did.json` : `${options.plcUrl}/${did}`;
|
|
27
|
+
identity = options.fetcher.json(url).then(({ data }) => ({
|
|
28
|
+
did,
|
|
29
|
+
pds: data.service?.find((service) => service.id.endsWith("#atproto_pds"))?.serviceEndpoint,
|
|
30
|
+
handle: data.alsoKnownAs?.find((alias) => alias.startsWith("at://"))?.slice(5)
|
|
31
|
+
}));
|
|
32
|
+
identity.catch(() => identities.delete(did));
|
|
33
|
+
identities.set(did, identity);
|
|
34
|
+
}
|
|
35
|
+
return abortable(identity, signal);
|
|
36
|
+
}
|
|
37
|
+
const handles = /* @__PURE__ */ new Map();
|
|
38
|
+
function resolveHandle(handle, signal) {
|
|
39
|
+
const key = handle.toLowerCase();
|
|
40
|
+
let did = handles.get(key);
|
|
41
|
+
if (!did) {
|
|
42
|
+
did = (async () => {
|
|
43
|
+
const { data } = await options.fetcher.json(`${options.handleResolverUrl}/xrpc/com.atproto.identity.resolveHandle`, { query: { handle: key } });
|
|
44
|
+
if ((await resolveDid(data.did)).handle?.toLowerCase() !== key) throw new NotFoundError(`${data.did} does not claim the handle ${handle}`, 404, "", options.context);
|
|
45
|
+
return data.did;
|
|
46
|
+
})();
|
|
47
|
+
did.catch(() => handles.delete(key));
|
|
48
|
+
handles.set(key, did);
|
|
49
|
+
}
|
|
50
|
+
return abortable(did, signal);
|
|
51
|
+
}
|
|
52
|
+
async function pdsOf(did, signal) {
|
|
53
|
+
const host = (await resolveDid(did, signal)).pds;
|
|
54
|
+
if (!host) throw new ForgeError(`No PDS in the DID document for ${did}`, options.context);
|
|
55
|
+
return host.replace(/\/$/, "");
|
|
56
|
+
}
|
|
57
|
+
async function* listRecords(did, collection, signal) {
|
|
58
|
+
const host = await pdsOf(did, signal);
|
|
59
|
+
let cursor;
|
|
60
|
+
do {
|
|
61
|
+
const { data } = await options.fetcher.json(`${host}/xrpc/com.atproto.repo.listRecords`, {
|
|
62
|
+
query: {
|
|
63
|
+
repo: did,
|
|
64
|
+
collection,
|
|
65
|
+
limit: 100,
|
|
66
|
+
cursor
|
|
67
|
+
},
|
|
68
|
+
signal
|
|
69
|
+
});
|
|
70
|
+
yield* data.records;
|
|
71
|
+
cursor = data.records.length ? data.cursor : void 0;
|
|
72
|
+
} while (cursor);
|
|
73
|
+
}
|
|
74
|
+
async function getRecord(uri, signal) {
|
|
75
|
+
const parsed = parseAtUri(uri);
|
|
76
|
+
if (!parsed) throw new ForgeError(`Not an AT-URI: ${uri}`, options.context);
|
|
77
|
+
const host = options.recordsUrl ?? await pdsOf(parsed.did, signal);
|
|
78
|
+
const { data } = await options.fetcher.json(`${host.replace(/\/$/, "")}/xrpc/com.atproto.repo.getRecord`, {
|
|
79
|
+
query: {
|
|
80
|
+
repo: parsed.did,
|
|
81
|
+
collection: parsed.collection,
|
|
82
|
+
rkey: parsed.rkey
|
|
83
|
+
},
|
|
84
|
+
mapError: (error) => error instanceof ForgeApiError && error.status === 400 ? new NotFoundError(`No record at ${uri}`, 400, error.body, options.context) : error,
|
|
85
|
+
signal
|
|
86
|
+
});
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
async function* backlinks(target, collection, path, signal) {
|
|
90
|
+
let cursor;
|
|
91
|
+
do {
|
|
92
|
+
const { data } = await options.fetcher.json(`${options.backlinksUrl}/links`, {
|
|
93
|
+
query: {
|
|
94
|
+
target,
|
|
95
|
+
collection,
|
|
96
|
+
path,
|
|
97
|
+
limit: 100,
|
|
98
|
+
cursor
|
|
99
|
+
},
|
|
100
|
+
headers: { accept: "application/json" },
|
|
101
|
+
signal
|
|
102
|
+
});
|
|
103
|
+
for (const link of data.linking_records) yield {
|
|
104
|
+
did: link.did,
|
|
105
|
+
collection: link.collection,
|
|
106
|
+
rkey: link.rkey
|
|
107
|
+
};
|
|
108
|
+
cursor = data.cursor ?? void 0;
|
|
109
|
+
} while (cursor);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
resolveDid,
|
|
113
|
+
resolveHandle,
|
|
114
|
+
getRecord,
|
|
115
|
+
listRecords,
|
|
116
|
+
backlinks
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/tangled/normalise.ts
|
|
121
|
+
const FORGE = "tangled";
|
|
122
|
+
const COLLECTIONS = {
|
|
123
|
+
repo: "sh.tangled.repo",
|
|
124
|
+
issue: "sh.tangled.repo.issue",
|
|
125
|
+
pull: "sh.tangled.repo.pull",
|
|
126
|
+
comment: "sh.tangled.feed.comment",
|
|
127
|
+
issueComment: "sh.tangled.repo.issue.comment",
|
|
128
|
+
pullComment: "sh.tangled.repo.pull.comment",
|
|
129
|
+
issueState: "sh.tangled.repo.issue.state",
|
|
130
|
+
pullStatus: "sh.tangled.repo.pull.status",
|
|
131
|
+
reaction: "sh.tangled.feed.reaction",
|
|
132
|
+
labelOp: "sh.tangled.label.op",
|
|
133
|
+
subscription: "sh.tangled.feed.subscription"
|
|
134
|
+
};
|
|
135
|
+
/** Collections that describe activity on an issue or pull request. */
|
|
136
|
+
const THREAD_COLLECTIONS = [
|
|
137
|
+
COLLECTIONS.issue,
|
|
138
|
+
COLLECTIONS.pull,
|
|
139
|
+
COLLECTIONS.comment,
|
|
140
|
+
COLLECTIONS.issueComment,
|
|
141
|
+
COLLECTIONS.pullComment,
|
|
142
|
+
COLLECTIONS.issueState,
|
|
143
|
+
COLLECTIONS.pullStatus,
|
|
144
|
+
COLLECTIONS.reaction,
|
|
145
|
+
COLLECTIONS.labelOp
|
|
146
|
+
];
|
|
147
|
+
function threadKindOf(collection) {
|
|
148
|
+
if (collection === COLLECTIONS.issue) return "issue";
|
|
149
|
+
return collection === COLLECTIONS.pull ? "pull_request" : void 0;
|
|
150
|
+
}
|
|
151
|
+
function toActor(instance, identity, webUrl) {
|
|
152
|
+
const login = identity.handle ?? identity.did;
|
|
153
|
+
return {
|
|
154
|
+
forge: FORGE,
|
|
155
|
+
instance,
|
|
156
|
+
login,
|
|
157
|
+
id: identity.did,
|
|
158
|
+
url: `${webUrl}/${login}`,
|
|
159
|
+
isBotHint: false
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function toThreadRef(instance, repo, uri) {
|
|
163
|
+
const value = atUri(uri.did, uri.collection, uri.rkey);
|
|
164
|
+
return {
|
|
165
|
+
forge: FORGE,
|
|
166
|
+
instance,
|
|
167
|
+
repo,
|
|
168
|
+
kind: threadKindOf(uri.collection) ?? "other",
|
|
169
|
+
typeRaw: uri.collection,
|
|
170
|
+
number: value,
|
|
171
|
+
externalId: value
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** The thread an activity record points at, as an AT-URI. */
|
|
175
|
+
function subjectOf(collection, record, uri) {
|
|
176
|
+
switch (collection) {
|
|
177
|
+
case COLLECTIONS.issue:
|
|
178
|
+
case COLLECTIONS.pull: return uri;
|
|
179
|
+
case COLLECTIONS.comment: return record.subject?.uri;
|
|
180
|
+
case COLLECTIONS.issueComment:
|
|
181
|
+
case COLLECTIONS.pullComment: return record.issue ?? record.pull;
|
|
182
|
+
case COLLECTIONS.issueState:
|
|
183
|
+
case COLLECTIONS.pullStatus: return record.issue ?? record.pull;
|
|
184
|
+
case COLLECTIONS.reaction:
|
|
185
|
+
case COLLECTIONS.labelOp: return record.subject;
|
|
186
|
+
default: return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Last segment of a `sh.tangled.repo.issue.state.closed`-style token. */
|
|
190
|
+
function stateToken(record) {
|
|
191
|
+
return (record.state ?? record.status)?.split(".").at(-1);
|
|
192
|
+
}
|
|
193
|
+
function toThreadState(token) {
|
|
194
|
+
switch (token) {
|
|
195
|
+
case "open":
|
|
196
|
+
case "closed":
|
|
197
|
+
case "merged": return token;
|
|
198
|
+
default: return "open";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function toEventKind(collection, operation) {
|
|
202
|
+
if (operation === "delete") return "other";
|
|
203
|
+
switch (collection) {
|
|
204
|
+
case COLLECTIONS.comment:
|
|
205
|
+
case COLLECTIONS.issueComment:
|
|
206
|
+
case COLLECTIONS.pullComment: return operation === "create" ? "comment" : "other";
|
|
207
|
+
case COLLECTIONS.issueState:
|
|
208
|
+
case COLLECTIONS.pullStatus: return "state_change";
|
|
209
|
+
case COLLECTIONS.issue: return operation === "create" ? "state_change" : "other";
|
|
210
|
+
case COLLECTIONS.pull: return operation === "create" ? "state_change" : "commit";
|
|
211
|
+
case COLLECTIONS.reaction: return "reaction";
|
|
212
|
+
case COLLECTIONS.labelOp: return "label";
|
|
213
|
+
default: return "other";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const STATE_ACTIONS = {
|
|
217
|
+
open: "reopened",
|
|
218
|
+
closed: "closed",
|
|
219
|
+
merged: "merged"
|
|
220
|
+
};
|
|
221
|
+
function toEventAction(activity) {
|
|
222
|
+
const { collection, operation } = activity;
|
|
223
|
+
if (operation === "delete") return "deleted";
|
|
224
|
+
switch (collection) {
|
|
225
|
+
case COLLECTIONS.issue: return operation === "create" ? "opened" : "edited";
|
|
226
|
+
case COLLECTIONS.pull: return operation === "create" ? "opened" : "synchronised";
|
|
227
|
+
case COLLECTIONS.issueState:
|
|
228
|
+
case COLLECTIONS.pullStatus: return STATE_ACTIONS[stateToken(activity.record) ?? ""] ?? "other";
|
|
229
|
+
default: return operation === "create" ? "created" : "edited";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function summarise(activity, kind, who) {
|
|
233
|
+
const { collection, record, operation } = activity;
|
|
234
|
+
if (operation === "delete") return `${who} deleted a ${collection.split(".").at(-1)}`;
|
|
235
|
+
switch (kind) {
|
|
236
|
+
case "comment": return `${who} commented`;
|
|
237
|
+
case "state_change":
|
|
238
|
+
if (collection === COLLECTIONS.issue) return `${who} opened an issue`;
|
|
239
|
+
if (collection === COLLECTIONS.pull) return `${who} opened a pull request`;
|
|
240
|
+
return `${who} marked it ${stateToken(record) ?? "changed"}`;
|
|
241
|
+
case "commit": return `${who} pushed a new round`;
|
|
242
|
+
case "reaction": return `${who} reacted ${record.reaction}`;
|
|
243
|
+
case "label": return `${who} changed labels`;
|
|
244
|
+
default: return `${who} edited a ${collection.split(".").at(-1)}`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function occurredAt(record) {
|
|
248
|
+
if (!record) return;
|
|
249
|
+
return toDate("performedAt" in record ? record.performedAt : record.createdAt);
|
|
250
|
+
}
|
|
251
|
+
function toActivityEvent(instance, activity, actor, thread, source, fallbackTime) {
|
|
252
|
+
const kind = toEventKind(activity.collection, activity.operation);
|
|
253
|
+
return {
|
|
254
|
+
forge: FORGE,
|
|
255
|
+
instance,
|
|
256
|
+
id: activity.operation === "create" || !activity.rev ? activity.uri : `${activity.uri}#${activity.rev}`,
|
|
257
|
+
kind,
|
|
258
|
+
kindRaw: activity.operation === "create" ? activity.collection : `${activity.collection}:${activity.operation}`,
|
|
259
|
+
action: toEventAction(activity),
|
|
260
|
+
actionRaw: activity.operation,
|
|
261
|
+
detail: kind === "state_change" && (activity.collection === COLLECTIONS.issueState || activity.collection === COLLECTIONS.pullStatus) ? {
|
|
262
|
+
type: "state_change",
|
|
263
|
+
state: toThreadState(stateToken(activity.record))
|
|
264
|
+
} : void 0,
|
|
265
|
+
summary: summarise(activity, kind, actor.login),
|
|
266
|
+
occurredAt: occurredAt(activity.record) ?? fallbackTime,
|
|
267
|
+
actor,
|
|
268
|
+
repo: thread?.repo,
|
|
269
|
+
thread,
|
|
270
|
+
source,
|
|
271
|
+
payload: activity.record ? {
|
|
272
|
+
uri: activity.uri,
|
|
273
|
+
...activity.record
|
|
274
|
+
} : { uri: activity.uri }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function toThread(ref, record, author, state, commentCount, warnings = []) {
|
|
278
|
+
const value = toThreadState(state.token);
|
|
279
|
+
const pull = "target" in record ? record : void 0;
|
|
280
|
+
const updatedAt = toDate(state.at) ?? toDate(pull?.rounds?.at(-1)?.createdAt) ?? toDate(record.createdAt);
|
|
281
|
+
return {
|
|
282
|
+
ref,
|
|
283
|
+
kind: ref.kind,
|
|
284
|
+
title: record.title,
|
|
285
|
+
body: record.body,
|
|
286
|
+
state: value,
|
|
287
|
+
stateRaw: state.token ?? "open",
|
|
288
|
+
isDraft: false,
|
|
289
|
+
author,
|
|
290
|
+
assignees: [],
|
|
291
|
+
reviewers: [],
|
|
292
|
+
createdAt: toDate(record.createdAt),
|
|
293
|
+
updatedAt,
|
|
294
|
+
closedAt: value === "open" ? void 0 : toDate(state.at),
|
|
295
|
+
lastActivityAt: updatedAt,
|
|
296
|
+
labels: [],
|
|
297
|
+
commentCount,
|
|
298
|
+
branches: pull ? {
|
|
299
|
+
head: { ref: pull.source?.branch ?? "" },
|
|
300
|
+
base: { ref: pull.target.branch }
|
|
301
|
+
} : void 0,
|
|
302
|
+
...warnings.length ? { warnings } : {},
|
|
303
|
+
raw: record
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function toRecordComment(thread, uri, record, author) {
|
|
307
|
+
const body = typeof record.body === "string" ? record.body : record.body.original ?? record.body.text ?? "";
|
|
308
|
+
return {
|
|
309
|
+
ref: {
|
|
310
|
+
forge: FORGE,
|
|
311
|
+
instance: thread.instance,
|
|
312
|
+
thread,
|
|
313
|
+
id: uri
|
|
314
|
+
},
|
|
315
|
+
body,
|
|
316
|
+
author,
|
|
317
|
+
createdAt: toDate(record.createdAt),
|
|
318
|
+
raw: {
|
|
319
|
+
uri,
|
|
320
|
+
...record
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const NOTIFICATION_REASONS = {
|
|
325
|
+
issue_created: "subscribed",
|
|
326
|
+
pull_created: "subscribed",
|
|
327
|
+
issue_commented: "comment",
|
|
328
|
+
pull_commented: "comment",
|
|
329
|
+
issue_closed: "state_change",
|
|
330
|
+
issue_reopen: "state_change",
|
|
331
|
+
pull_closed: "state_change",
|
|
332
|
+
pull_reopen: "state_change",
|
|
333
|
+
pull_merged: "state_change",
|
|
334
|
+
issue_assigned: "assigned",
|
|
335
|
+
pull_assigned: "assigned",
|
|
336
|
+
user_mentioned: "mention",
|
|
337
|
+
repo_starred: "starred",
|
|
338
|
+
followed: "followed"
|
|
339
|
+
};
|
|
340
|
+
function toNotificationReason(type) {
|
|
341
|
+
return NOTIFICATION_REASONS[type] ?? "unknown";
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/tangled/notifications.ts
|
|
345
|
+
const NOTIFICATION_NSID = {
|
|
346
|
+
list: "org.tangled.temp.notification.listNotifications",
|
|
347
|
+
updateSeen: "org.tangled.temp.notification.updateSeen",
|
|
348
|
+
markAllRead: "org.tangled.temp.notification.markAllRead",
|
|
349
|
+
unreadCount: "org.tangled.temp.notification.getUnreadCount"
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* `org.tangled.temp.notification.*` is served by a separate service that
|
|
353
|
+
* authenticates callers with atproto service-auth tokens minted by their
|
|
354
|
+
* PDS for audience `did:web:<host>`.
|
|
355
|
+
*/
|
|
356
|
+
function createTangledNotifications(url, { instance, context, fetcher, atproto, pdsCall, resolveRepo, actorFor }) {
|
|
357
|
+
const audience = `did:web:${new URL(url).host}`;
|
|
358
|
+
const titles = /* @__PURE__ */ new Map();
|
|
359
|
+
async function call(nsid, init = {}) {
|
|
360
|
+
const { token } = await pdsCall("com.atproto.server.getServiceAuth", {
|
|
361
|
+
query: {
|
|
362
|
+
aud: audience,
|
|
363
|
+
lxm: nsid
|
|
364
|
+
},
|
|
365
|
+
signal: init.signal
|
|
366
|
+
});
|
|
367
|
+
const { data } = await fetcher.json(`${url}/xrpc/${nsid}`, {
|
|
368
|
+
...init,
|
|
369
|
+
headers: {
|
|
370
|
+
...init.headers,
|
|
371
|
+
authorization: `Bearer ${token}`
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return data;
|
|
375
|
+
}
|
|
376
|
+
function titleOf(subject) {
|
|
377
|
+
let title = titles.get(subject);
|
|
378
|
+
if (!title) {
|
|
379
|
+
title = atproto.getRecord(subject).then(({ value }) => value.title, () => void 0);
|
|
380
|
+
titles.set(subject, title);
|
|
381
|
+
}
|
|
382
|
+
return title;
|
|
383
|
+
}
|
|
384
|
+
async function toItem(item) {
|
|
385
|
+
const subject = item.issueAt ?? item.pullAt;
|
|
386
|
+
const parsed = subject ? parseAtUri(subject) : void 0;
|
|
387
|
+
const repo = item.repoDid ? await resolveRepo(item.repoDid).catch(() => ({
|
|
388
|
+
forge: FORGE,
|
|
389
|
+
instance,
|
|
390
|
+
owner: item.repoDid,
|
|
391
|
+
name: "",
|
|
392
|
+
externalId: item.repoDid
|
|
393
|
+
})) : void 0;
|
|
394
|
+
let notificationSubject;
|
|
395
|
+
if (parsed && repo) notificationSubject = {
|
|
396
|
+
type: "thread",
|
|
397
|
+
thread: toThreadRef(instance, repo, parsed)
|
|
398
|
+
};
|
|
399
|
+
else if (item.type === "followed") notificationSubject = {
|
|
400
|
+
type: "actor",
|
|
401
|
+
actor: await actorFor(item.actorDid)
|
|
402
|
+
};
|
|
403
|
+
else if (repo) notificationSubject = {
|
|
404
|
+
type: "repo",
|
|
405
|
+
repo
|
|
406
|
+
};
|
|
407
|
+
else notificationSubject = {
|
|
408
|
+
type: "other",
|
|
409
|
+
typeRaw: item.type,
|
|
410
|
+
id: item.uri
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
ref: {
|
|
414
|
+
forge: FORGE,
|
|
415
|
+
instance,
|
|
416
|
+
id: item.uri
|
|
417
|
+
},
|
|
418
|
+
subject: notificationSubject,
|
|
419
|
+
subjectTypeRaw: item.type,
|
|
420
|
+
reason: toNotificationReason(item.type),
|
|
421
|
+
reasonRaw: item.type,
|
|
422
|
+
unread: !item.read,
|
|
423
|
+
title: (subject ? await titleOf(subject) : void 0) ?? item.type.replaceAll("_", " "),
|
|
424
|
+
subjectState: "unknown",
|
|
425
|
+
updatedAt: toDate(item.createdAt) ?? /* @__PURE__ */ new Date(0),
|
|
426
|
+
raw: item
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async function page(listOptions = {}) {
|
|
430
|
+
const data = await call(NOTIFICATION_NSID.list, {
|
|
431
|
+
query: {
|
|
432
|
+
read: listOptions.all ? "all" : "unread",
|
|
433
|
+
limit: listOptions.perPage ?? 100
|
|
434
|
+
},
|
|
435
|
+
signal: listOptions.signal
|
|
436
|
+
});
|
|
437
|
+
const since = listOptions.since?.getTime();
|
|
438
|
+
return { items: await Promise.all(data.notifications.filter((item) => since === void 0 || (toDate(item.createdAt)?.getTime() ?? 0) >= since).map(toItem)) };
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
listPage: verb("experimental", page),
|
|
442
|
+
markRead: verb("experimental", async (ref, options) => {
|
|
443
|
+
await call(NOTIFICATION_NSID.updateSeen, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
json: {
|
|
446
|
+
uri: ref.id,
|
|
447
|
+
read: true
|
|
448
|
+
},
|
|
449
|
+
signal: options?.signal
|
|
450
|
+
});
|
|
451
|
+
}),
|
|
452
|
+
markAllRead: verb("unverified", async (bulk = {}) => {
|
|
453
|
+
if (bulk.repo || bulk.before) throw new UnsupportedOperationError("Tangled marks every notification read at once; repo and before filters are not supported", context);
|
|
454
|
+
await call(NOTIFICATION_NSID.markAllRead, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
signal: bulk.signal
|
|
457
|
+
});
|
|
458
|
+
}),
|
|
459
|
+
unreadCount: verb("unverified", async () => (await call(NOTIFICATION_NSID.unreadCount)).count)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/tangled/records.ts
|
|
464
|
+
const TANGLED_DID = "did:plc:wshs7t2adsemcrrd4snkeqli";
|
|
465
|
+
/** Knots that accept git over SSH on a different host. */
|
|
466
|
+
const DEFAULT_SSH_HOSTS = { "knot1.tangled.sh": "tangled.org" };
|
|
467
|
+
const STATE_COLLECTION = {
|
|
468
|
+
issue: {
|
|
469
|
+
collection: COLLECTIONS.issueState,
|
|
470
|
+
path: ".issue"
|
|
471
|
+
},
|
|
472
|
+
pull_request: {
|
|
473
|
+
collection: COLLECTIONS.pullStatus,
|
|
474
|
+
path: ".pull"
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const COMMENT_SOURCES = {
|
|
478
|
+
issue: [{
|
|
479
|
+
collection: COLLECTIONS.comment,
|
|
480
|
+
path: ".subject.uri"
|
|
481
|
+
}, {
|
|
482
|
+
collection: COLLECTIONS.issueComment,
|
|
483
|
+
path: ".issue"
|
|
484
|
+
}],
|
|
485
|
+
pull_request: [{
|
|
486
|
+
collection: COLLECTIONS.comment,
|
|
487
|
+
path: ".subject.uri"
|
|
488
|
+
}, {
|
|
489
|
+
collection: COLLECTIONS.pullComment,
|
|
490
|
+
path: ".pull"
|
|
491
|
+
}]
|
|
492
|
+
};
|
|
493
|
+
function ACTIVITY_SOURCES(kind) {
|
|
494
|
+
return [
|
|
495
|
+
...COMMENT_SOURCES[kind],
|
|
496
|
+
STATE_COLLECTION[kind],
|
|
497
|
+
{
|
|
498
|
+
collection: COLLECTIONS.reaction,
|
|
499
|
+
path: ".subject"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
collection: COLLECTIONS.labelOp,
|
|
503
|
+
path: ".subject"
|
|
504
|
+
}
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
function createTangledRecords({ options, instance, webUrl, context, atproto, api, fetcher, ownRecords }) {
|
|
508
|
+
const repos = /* @__PURE__ */ new Map();
|
|
509
|
+
const collaborators = /* @__PURE__ */ new Map();
|
|
510
|
+
/**
|
|
511
|
+
* Repo collaborators, from the knot that hosts the repo DID. Knots older
|
|
512
|
+
* than v1.15 do not serve this, and an empty set is returned.
|
|
513
|
+
*/
|
|
514
|
+
function collaboratorsOf(repo, signal) {
|
|
515
|
+
const repoDid = repo.externalId;
|
|
516
|
+
if (!repoDid) return Promise.resolve({ dids: /* @__PURE__ */ new Set() });
|
|
517
|
+
let found = collaborators.get(repoDid);
|
|
518
|
+
if (!found) {
|
|
519
|
+
found = (async () => {
|
|
520
|
+
const { pds: knot } = await atproto.resolveDid(repoDid);
|
|
521
|
+
const dids = /* @__PURE__ */ new Set();
|
|
522
|
+
if (!knot) return { dids };
|
|
523
|
+
let cursor;
|
|
524
|
+
do {
|
|
525
|
+
const { data } = await fetcher.json(`${knot.replace(/\/$/, "")}/xrpc/sh.tangled.repo.listCollaborators`, { query: {
|
|
526
|
+
subject: repoDid,
|
|
527
|
+
limit: 1e3,
|
|
528
|
+
cursor
|
|
529
|
+
} });
|
|
530
|
+
for (const item of data.items) dids.add(item.subject);
|
|
531
|
+
cursor = data.cursor;
|
|
532
|
+
} while (cursor);
|
|
533
|
+
return { dids };
|
|
534
|
+
})().catch((error) => {
|
|
535
|
+
if (error instanceof ForgeApiError && !(error instanceof RateLimitedError)) return { dids: /* @__PURE__ */ new Set() };
|
|
536
|
+
if (error instanceof ForgeError) return {
|
|
537
|
+
dids: /* @__PURE__ */ new Set(),
|
|
538
|
+
warning: toWarning("collaborators_unreachable", error, repoDid)
|
|
539
|
+
};
|
|
540
|
+
throw error;
|
|
541
|
+
});
|
|
542
|
+
collaborators.set(repoDid, found);
|
|
543
|
+
}
|
|
544
|
+
return abortable(found, signal);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Accounts whose state records the appview accepts: the thread author, the
|
|
548
|
+
* repo owner, collaborators with push access, and Tangled's own account.
|
|
549
|
+
*/
|
|
550
|
+
async function stateAuthors(authorDid, repo, warn, signal) {
|
|
551
|
+
const { dids, warning } = await collaboratorsOf(repo, signal);
|
|
552
|
+
if (warning) warn?.(warning);
|
|
553
|
+
return /* @__PURE__ */ new Set([
|
|
554
|
+
authorDid,
|
|
555
|
+
repo.owner,
|
|
556
|
+
TANGLED_DID,
|
|
557
|
+
...dids
|
|
558
|
+
]);
|
|
559
|
+
}
|
|
560
|
+
async function actorFor(did, signal) {
|
|
561
|
+
return toActor(instance, await atproto.resolveDid(did, signal).catch((error) => {
|
|
562
|
+
if (signal?.aborted) throw error;
|
|
563
|
+
return { did };
|
|
564
|
+
}), webUrl);
|
|
565
|
+
}
|
|
566
|
+
/** Resolves a record's `repo` field, a repo DID or, in older records, an `sh.tangled.repo` AT-URI. */
|
|
567
|
+
function resolveRepo(field, signal) {
|
|
568
|
+
let repo = repos.get(field);
|
|
569
|
+
if (!repo) {
|
|
570
|
+
repo = (async () => {
|
|
571
|
+
const legacy = parseAtUri(field);
|
|
572
|
+
if (legacy) return {
|
|
573
|
+
forge: FORGE,
|
|
574
|
+
instance,
|
|
575
|
+
owner: legacy.did,
|
|
576
|
+
name: legacy.rkey
|
|
577
|
+
};
|
|
578
|
+
const { value: link } = await atproto.backlinks(field, COLLECTIONS.repo, ".repoDid").next();
|
|
579
|
+
if (!link) throw new UnresolvedThreadError(`No sh.tangled.repo record declares ${field}`, context);
|
|
580
|
+
return {
|
|
581
|
+
forge: FORGE,
|
|
582
|
+
instance,
|
|
583
|
+
owner: link.did,
|
|
584
|
+
name: link.rkey,
|
|
585
|
+
externalId: field
|
|
586
|
+
};
|
|
587
|
+
})();
|
|
588
|
+
repo.catch(() => repos.delete(field));
|
|
589
|
+
repos.set(field, repo);
|
|
590
|
+
}
|
|
591
|
+
return abortable(repo, signal);
|
|
592
|
+
}
|
|
593
|
+
async function threadFor(subject, signal) {
|
|
594
|
+
const uri = parseAtUri(subject);
|
|
595
|
+
if (!uri || !threadKindOf(uri.collection)) throw new UnresolvedThreadError(`${subject} is not an issue or pull request`, context);
|
|
596
|
+
const { value, cid } = await atproto.getRecord(subject, signal);
|
|
597
|
+
return {
|
|
598
|
+
ref: toThreadRef(instance, await resolveRepo("target" in value ? value.target.repo : value.repo, signal), uri),
|
|
599
|
+
record: value,
|
|
600
|
+
cid
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function subjectUri(thread) {
|
|
604
|
+
const ref = requireThread(thread, context);
|
|
605
|
+
const uri = parseAtUri(ref.number);
|
|
606
|
+
if (!uri || ref.kind !== "issue" && ref.kind !== "pull_request" || threadKindOf(uri.collection) !== ref.kind) throw new UnresolvedThreadError("Tangled thread refs carry the issue or pull AT-URI as their number", context);
|
|
607
|
+
return {
|
|
608
|
+
ref,
|
|
609
|
+
uri,
|
|
610
|
+
kind: ref.kind
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Reads one record that another record points at. Any failure is a
|
|
615
|
+
* warning: each record lives on its author's own PDS, and one unreachable
|
|
616
|
+
* participant must not fail the whole read. Deleted records are skipped
|
|
617
|
+
* silently.
|
|
618
|
+
*/
|
|
619
|
+
async function readLinked(uri, warn, signal) {
|
|
620
|
+
try {
|
|
621
|
+
return (await atproto.getRecord(uri, signal)).value;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (error instanceof NotFoundError) return;
|
|
624
|
+
if (!degradesToWarning(error)) throw error;
|
|
625
|
+
warn(toWarning("record_unreachable", error, uri));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async function* activity(target, kind, allowedState, warn, { sources = ACTIVITY_SOURCES(kind), signal } = {}) {
|
|
630
|
+
for (const source of sources) for await (const link of atproto.backlinks(target, source.collection, source.path, signal)) {
|
|
631
|
+
if (source === STATE_COLLECTION[kind] && !allowedState.has(link.did)) continue;
|
|
632
|
+
const uri = atUri(link.did, link.collection, link.rkey);
|
|
633
|
+
const record = await readLinked(uri, warn, signal);
|
|
634
|
+
if (record) yield {
|
|
635
|
+
uri,
|
|
636
|
+
collection: link.collection,
|
|
637
|
+
record
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async function stateOf(target, kind, allowed, warn, signal) {
|
|
642
|
+
let latest = {};
|
|
643
|
+
const source = STATE_COLLECTION[kind];
|
|
644
|
+
for await (const link of atproto.backlinks(target, source.collection, source.path, signal)) {
|
|
645
|
+
if (!allowed.has(link.did)) continue;
|
|
646
|
+
const value = await readLinked(atUri(link.did, link.collection, link.rkey), (warning) => {
|
|
647
|
+
warn({
|
|
648
|
+
...warning,
|
|
649
|
+
code: "state_record_unreachable",
|
|
650
|
+
message: `State may be stale: ${warning.message}`
|
|
651
|
+
});
|
|
652
|
+
}, signal);
|
|
653
|
+
if (!value) continue;
|
|
654
|
+
if (!latest.at || (toDate(value.createdAt)?.getTime() ?? 0) > (toDate(latest.at)?.getTime() ?? 0)) latest = {
|
|
655
|
+
token: stateToken(value),
|
|
656
|
+
at: value.createdAt
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return latest;
|
|
660
|
+
}
|
|
661
|
+
async function countComments(target, kind, signal) {
|
|
662
|
+
let count = 0;
|
|
663
|
+
for (const source of COMMENT_SOURCES[kind]) for await (const _ of atproto.backlinks(target, source.collection, source.path, signal)) count++;
|
|
664
|
+
return count;
|
|
665
|
+
}
|
|
666
|
+
async function toStreamEvent(message) {
|
|
667
|
+
const { commit } = message;
|
|
668
|
+
const uri = atUri(message.did, commit.collection, commit.rkey);
|
|
669
|
+
const subject = commit.record ? subjectOf(commit.collection, commit.record, uri) : void 0;
|
|
670
|
+
const thread = subject ? await threadFor(subject).then(({ ref }) => ref, () => void 0) : void 0;
|
|
671
|
+
return toActivityEvent(instance, {
|
|
672
|
+
uri,
|
|
673
|
+
collection: commit.collection,
|
|
674
|
+
record: commit.record,
|
|
675
|
+
operation: commit.operation,
|
|
676
|
+
rev: commit.rev
|
|
677
|
+
}, await actorFor(message.did), thread, "subscribe", /* @__PURE__ */ new Date(message.time_us / 1e3));
|
|
678
|
+
}
|
|
679
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
680
|
+
/** The repo record `repo` names, where `owner` may be a handle and `name` the record's name rather than its key. */
|
|
681
|
+
function canonicalRepo(repo, signal) {
|
|
682
|
+
const key = `${repo.owner}/${repo.name}`;
|
|
683
|
+
let found = canonical.get(key);
|
|
684
|
+
if (!found) {
|
|
685
|
+
found = (async () => {
|
|
686
|
+
const did = repo.owner.startsWith("did:") ? repo.owner : await atproto.resolveHandle(repo.owner.replace(/^@/, ""));
|
|
687
|
+
const record = await atproto.getRecord(atUri(did, COLLECTIONS.repo, repo.name)).catch((error) => {
|
|
688
|
+
if (error instanceof NotFoundError) return;
|
|
689
|
+
throw error;
|
|
690
|
+
});
|
|
691
|
+
if (record) return {
|
|
692
|
+
ref: {
|
|
693
|
+
...repo,
|
|
694
|
+
owner: did
|
|
695
|
+
},
|
|
696
|
+
record
|
|
697
|
+
};
|
|
698
|
+
for await (const candidate of atproto.listRecords(did, COLLECTIONS.repo)) if (candidate.value.name === repo.name) return {
|
|
699
|
+
ref: {
|
|
700
|
+
...repo,
|
|
701
|
+
owner: did,
|
|
702
|
+
name: parseAtUri(candidate.uri).rkey
|
|
703
|
+
},
|
|
704
|
+
record: candidate
|
|
705
|
+
};
|
|
706
|
+
throw new NotFoundError(`No Tangled repo named ${repo.name} for ${repo.owner}`, 404, "", context);
|
|
707
|
+
})();
|
|
708
|
+
found.catch(() => canonical.delete(key));
|
|
709
|
+
canonical.set(key, found);
|
|
710
|
+
}
|
|
711
|
+
return abortable(found, signal);
|
|
712
|
+
}
|
|
713
|
+
/** `repo` with a DID owner and the record key as `name`; refs this provider returned already are. */
|
|
714
|
+
async function canonicalRef(repo, signal) {
|
|
715
|
+
return repo.externalId && repo.owner.startsWith("did:") ? repo : (await canonicalRepo(repo, signal)).ref;
|
|
716
|
+
}
|
|
717
|
+
async function repoDidOf(repo, signal) {
|
|
718
|
+
if (repo.externalId) return repo.externalId;
|
|
719
|
+
const { record: { value } } = await canonicalRepo(repo, signal);
|
|
720
|
+
if (!value.repoDid) throw new UnresolvedThreadError(`${repo.owner}/${repo.name} has no repo DID`, context);
|
|
721
|
+
return value.repoDid;
|
|
722
|
+
}
|
|
723
|
+
function sshHost(knot) {
|
|
724
|
+
return options.sshHosts?.[knot] ?? DEFAULT_SSH_HOSTS[knot] ?? knot;
|
|
725
|
+
}
|
|
726
|
+
function toRepo(ref, record, raw) {
|
|
727
|
+
const parent = record.source ? parseAtUri(record.source) : void 0;
|
|
728
|
+
const path = `${ref.owner}/${ref.name}`;
|
|
729
|
+
return {
|
|
730
|
+
ref: record.repoDid ? {
|
|
731
|
+
...ref,
|
|
732
|
+
externalId: record.repoDid
|
|
733
|
+
} : ref,
|
|
734
|
+
displayName: record.name && record.name !== ref.name ? record.name : void 0,
|
|
735
|
+
description: record.description,
|
|
736
|
+
visibility: "public",
|
|
737
|
+
isFork: Boolean(record.source),
|
|
738
|
+
isArchived: false,
|
|
739
|
+
parent: parent ? {
|
|
740
|
+
forge: FORGE,
|
|
741
|
+
instance,
|
|
742
|
+
owner: parent.did,
|
|
743
|
+
name: parent.rkey
|
|
744
|
+
} : void 0,
|
|
745
|
+
topics: record.topics ?? [],
|
|
746
|
+
url: `${webUrl}/${path}`,
|
|
747
|
+
cloneUrls: {
|
|
748
|
+
https: `${webUrl}/${path}`,
|
|
749
|
+
ssh: `ssh://git@${sshHost(record.knot)}/${path}`
|
|
750
|
+
},
|
|
751
|
+
createdAt: toDate(record.createdAt),
|
|
752
|
+
homepage: record.website || void 0,
|
|
753
|
+
raw
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
async function readThread(thread, options) {
|
|
757
|
+
const { uri, kind } = subjectUri(thread);
|
|
758
|
+
const target = atUri(uri.did, uri.collection, uri.rkey);
|
|
759
|
+
const { ref, record } = await threadFor(target, options?.signal);
|
|
760
|
+
const warnings = [];
|
|
761
|
+
const warn = (warning) => warnings.push(warning);
|
|
762
|
+
const [author, state, comments] = await Promise.all([
|
|
763
|
+
actorFor(uri.did, options?.signal),
|
|
764
|
+
stateAuthors(uri.did, ref.repo, warn, options?.signal).then((allowed) => stateOf(target, kind, allowed, warn, options?.signal)),
|
|
765
|
+
countComments(target, kind, options?.signal)
|
|
766
|
+
]);
|
|
767
|
+
return toThread(ref, record, author, state, comments, warnings);
|
|
768
|
+
}
|
|
769
|
+
async function listFromIndex(repo, query) {
|
|
770
|
+
const warnings = [];
|
|
771
|
+
if (!query.cursor) {
|
|
772
|
+
warnings.push({
|
|
773
|
+
code: "index_possibly_stale",
|
|
774
|
+
message: "Listed from the Bobbin index; states may lag behind the records"
|
|
775
|
+
});
|
|
776
|
+
if (query.sort && query.sort !== "created") warnings.push({
|
|
777
|
+
code: "sort_unsupported",
|
|
778
|
+
message: "The index sorts by creation time only"
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
const repoDid = await repoDidOf(repo, query.signal);
|
|
782
|
+
const state = query.state ?? "open";
|
|
783
|
+
const kinds = query.kind ? [query.kind] : ["issue", "pull_request"];
|
|
784
|
+
const page = await phased(kinds.map((kind) => async (cursor) => {
|
|
785
|
+
const pulls = kind === "pull_request";
|
|
786
|
+
const { data } = await api.json(`/xrpc/sh.tangled.repo.${pulls ? "listPulls" : "listIssues"}`, {
|
|
787
|
+
query: {
|
|
788
|
+
subject: repoDid,
|
|
789
|
+
[pulls ? "status" : "state"]: state === "all" || pulls && state === "closed" ? void 0 : state,
|
|
790
|
+
author: query.author?.startsWith("did:") ? query.author : void 0,
|
|
791
|
+
order: query.direction ?? "desc",
|
|
792
|
+
limit: query.perPage ?? 50,
|
|
793
|
+
cursor: cursor?.token
|
|
794
|
+
},
|
|
795
|
+
signal: query.signal
|
|
796
|
+
});
|
|
797
|
+
const items = [];
|
|
798
|
+
for (const item of data.items) {
|
|
799
|
+
const uri = parseAtUri(item.uri);
|
|
800
|
+
if (!uri || state === "closed" && item.state === "open") continue;
|
|
801
|
+
const author = await actorFor(uri.did, query.signal);
|
|
802
|
+
if (query.author && author.login !== query.author && author.id !== query.author) continue;
|
|
803
|
+
const thread = toThread(toThreadRef(instance, {
|
|
804
|
+
...repo,
|
|
805
|
+
externalId: repoDid
|
|
806
|
+
}, uri), item.value, author, {
|
|
807
|
+
token: item.state,
|
|
808
|
+
at: item.stateUpdatedAt
|
|
809
|
+
}, item.commentCount);
|
|
810
|
+
if (query.since && (thread.lastActivityAt?.getTime() ?? 0) < query.since.getTime() || query.createdAfter && (thread.createdAt?.getTime() ?? 0) < query.createdAfter.getTime()) continue;
|
|
811
|
+
items.push(thread);
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
items,
|
|
815
|
+
cursor: data.items.length && data.cursor ? { token: data.cursor } : void 0
|
|
816
|
+
};
|
|
817
|
+
}), query.cursor);
|
|
818
|
+
return warnings.length ? {
|
|
819
|
+
...page,
|
|
820
|
+
warnings: [...warnings, ...page.warnings ?? []]
|
|
821
|
+
} : page;
|
|
822
|
+
}
|
|
823
|
+
/** Record-based listings read every record to apply the appview's rules, so they return one complete page. */
|
|
824
|
+
async function wholePage(iterable) {
|
|
825
|
+
const items = await Array.fromAsync(iterable);
|
|
826
|
+
return iterable.warnings.length ? {
|
|
827
|
+
items,
|
|
828
|
+
warnings: iterable.warnings
|
|
829
|
+
} : { items };
|
|
830
|
+
}
|
|
831
|
+
function listPage(repo, query = {}) {
|
|
832
|
+
return options.listSource === "index" && query.kind !== "discussion" ? listFromIndex(repo, query) : wholePage(listThreads(repo, query));
|
|
833
|
+
}
|
|
834
|
+
function listThreads(repo, query = {}) {
|
|
835
|
+
return forgeIterable(async function* (warn) {
|
|
836
|
+
if (query.kind === "discussion") {
|
|
837
|
+
warn({
|
|
838
|
+
code: "kind_unsupported",
|
|
839
|
+
message: "Tangled has no discussions"
|
|
840
|
+
});
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (query.labels?.length || query.assignee) warn({
|
|
844
|
+
code: "filter_unsupported",
|
|
845
|
+
message: "Tangled labels and assignees are not read yet; those filters were ignored"
|
|
846
|
+
});
|
|
847
|
+
const address = await canonicalRef(repo, query.signal);
|
|
848
|
+
const targets = [await repoDidOf(repo, query.signal), atUri(address.owner, COLLECTIONS.repo, address.name)];
|
|
849
|
+
const uris = [];
|
|
850
|
+
for (const kind of query.kind ? [query.kind] : ["issue", "pull_request"]) {
|
|
851
|
+
const collection = kind === "issue" ? COLLECTIONS.issue : COLLECTIONS.pull;
|
|
852
|
+
for (const target of targets) for await (const link of atproto.backlinks(target, collection, kind === "issue" ? ".repo" : ".target.repo", query.signal)) uris.push(atUri(link.did, link.collection, link.rkey));
|
|
853
|
+
}
|
|
854
|
+
const threadsRead = await mapConcurrent([...new Set(uris)], 4, async (uri) => {
|
|
855
|
+
const parsed = parseAtUri(uri);
|
|
856
|
+
try {
|
|
857
|
+
const thread = await readThread({
|
|
858
|
+
forge: FORGE,
|
|
859
|
+
instance,
|
|
860
|
+
repo,
|
|
861
|
+
kind: threadKindOf(parsed.collection),
|
|
862
|
+
number: uri
|
|
863
|
+
}, query);
|
|
864
|
+
thread.warnings?.forEach(warn);
|
|
865
|
+
return thread;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (!degradesToWarning(error)) throw error;
|
|
868
|
+
warn(toWarning("thread_unreadable", error, uri));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
const state = query.state ?? "open";
|
|
873
|
+
const sortKey = (thread) => query.sort === "comments" ? thread.commentCount ?? 0 : (query.sort === "updated" ? thread.lastActivityAt : thread.createdAt)?.getTime() ?? 0;
|
|
874
|
+
const direction = (query.direction ?? "desc") === "desc" ? -1 : 1;
|
|
875
|
+
yield* threadsRead.filter((thread) => Boolean(thread)).filter((thread) => state === "all" || (state === "open" ? thread.state === "open" : state === "merged" ? thread.state === "merged" : thread.state !== "open")).filter((thread) => !query.author || thread.author?.login === query.author || thread.author?.id === query.author).filter((thread) => !query.since || (thread.lastActivityAt?.getTime() ?? 0) >= query.since.getTime()).filter((thread) => !query.createdAfter || (thread.createdAt?.getTime() ?? 0) >= query.createdAfter.getTime()).sort((a, b) => (sortKey(a) - sortKey(b)) * direction);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function listComments(thread, options = {}) {
|
|
879
|
+
const { ref, uri, kind } = subjectUri(thread);
|
|
880
|
+
const target = atUri(uri.did, uri.collection, uri.rkey);
|
|
881
|
+
return forgeIterable(async function* (warn) {
|
|
882
|
+
const comments = [];
|
|
883
|
+
for await (const item of activity(target, kind, /* @__PURE__ */ new Set(), warn, {
|
|
884
|
+
sources: COMMENT_SOURCES[kind],
|
|
885
|
+
signal: options.signal
|
|
886
|
+
})) comments.push(toRecordComment(ref, item.uri, item.record, await actorFor(parseAtUri(item.uri).did, options.signal)));
|
|
887
|
+
yield* comments.sort((a, b) => (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0));
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
async function findSubscription(thread, signal) {
|
|
891
|
+
const { uri } = subjectUri(thread);
|
|
892
|
+
const target = atUri(uri.did, uri.collection, uri.rkey);
|
|
893
|
+
for await (const record of ownRecords(COLLECTIONS.subscription, signal)) if (record.value.subject.uri === target) return record.uri;
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
stateAuthors,
|
|
897
|
+
actorFor,
|
|
898
|
+
resolveRepo,
|
|
899
|
+
threadFor,
|
|
900
|
+
subjectUri,
|
|
901
|
+
activity,
|
|
902
|
+
toStreamEvent,
|
|
903
|
+
canonicalRepo,
|
|
904
|
+
canonicalRef,
|
|
905
|
+
repoDidOf,
|
|
906
|
+
toRepo,
|
|
907
|
+
readThread,
|
|
908
|
+
wholePage,
|
|
909
|
+
listPage,
|
|
910
|
+
listComments,
|
|
911
|
+
findSubscription
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/tangled/session.ts
|
|
916
|
+
function createTangledSession({ options, fetcher, atproto, context }) {
|
|
917
|
+
const writable = options.auth !== void 0 && options.auth.type !== "anonymous";
|
|
918
|
+
let session;
|
|
919
|
+
let pdsUrl;
|
|
920
|
+
async function createSession(refresh) {
|
|
921
|
+
const auth = options.auth;
|
|
922
|
+
if (!pdsUrl) {
|
|
923
|
+
const did = auth.pds ? void 0 : auth.identifier.startsWith("did:") ? auth.identifier : await atproto.resolveHandle(auth.identifier).catch(() => void 0);
|
|
924
|
+
pdsUrl = auth.pds ?? (did ? (await atproto.resolveDid(did)).pds : void 0) ?? "https://bsky.social";
|
|
925
|
+
}
|
|
926
|
+
const { data } = await fetcher.json(`${pdsUrl}/xrpc/com.atproto.server.${refresh ? "refreshSession" : "createSession"}`, refresh ? {
|
|
927
|
+
method: "POST",
|
|
928
|
+
headers: { authorization: `Bearer ${refresh.refreshJwt}` }
|
|
929
|
+
} : {
|
|
930
|
+
method: "POST",
|
|
931
|
+
json: {
|
|
932
|
+
identifier: auth.identifier,
|
|
933
|
+
password: auth.password
|
|
934
|
+
},
|
|
935
|
+
mapError: rejectedPassword
|
|
936
|
+
});
|
|
937
|
+
return data;
|
|
938
|
+
}
|
|
939
|
+
async function currentSession() {
|
|
940
|
+
session ??= createSession();
|
|
941
|
+
session.catch(() => {
|
|
942
|
+
session = void 0;
|
|
943
|
+
});
|
|
944
|
+
return session;
|
|
945
|
+
}
|
|
946
|
+
async function viewerDid() {
|
|
947
|
+
const auth = options.auth;
|
|
948
|
+
return auth.type === "oauth" ? auth.did : (await currentSession()).did;
|
|
949
|
+
}
|
|
950
|
+
/** Calls an XRPC method on the authenticated account's PDS, refreshing an expired session once. */
|
|
951
|
+
async function pdsCall(nsid, init = {}) {
|
|
952
|
+
const auth = options.auth;
|
|
953
|
+
if (auth.type === "oauth") return (await createFetcher({
|
|
954
|
+
baseUrl: auth.pds,
|
|
955
|
+
fetch: auth.fetch,
|
|
956
|
+
authenticated: true,
|
|
957
|
+
timeout: options.timeout,
|
|
958
|
+
context
|
|
959
|
+
}).json(`${auth.pds.replace(/\/$/, "")}/xrpc/${nsid}`, init)).data;
|
|
960
|
+
const send = async (current) => (await fetcher.json(`${pdsUrl}/xrpc/${nsid}`, {
|
|
961
|
+
...init,
|
|
962
|
+
headers: {
|
|
963
|
+
...init.headers,
|
|
964
|
+
authorization: `Bearer ${current.accessJwt}`
|
|
965
|
+
}
|
|
966
|
+
})).data;
|
|
967
|
+
const current = await currentSession();
|
|
968
|
+
try {
|
|
969
|
+
return await send(current);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (!(error instanceof ForgeApiError) || error.status !== 400 || !error.body.includes("ExpiredToken")) throw error;
|
|
972
|
+
session = createSession(current);
|
|
973
|
+
return send(await session);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function createRecord(collection, record, signal) {
|
|
977
|
+
return pdsCall("com.atproto.repo.createRecord", {
|
|
978
|
+
method: "POST",
|
|
979
|
+
json: {
|
|
980
|
+
repo: await viewerDid(),
|
|
981
|
+
collection,
|
|
982
|
+
record: {
|
|
983
|
+
$type: collection,
|
|
984
|
+
...record
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
signal
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
/** Rewrites one of the account's own records, refusing records it does not own. */
|
|
991
|
+
async function putOwnRecord(uri, update, signal) {
|
|
992
|
+
const parsed = parseAtUri(uri);
|
|
993
|
+
const viewer = await viewerDid();
|
|
994
|
+
if (!parsed || parsed.did !== viewer) throw new InsufficientScopeError("Records can only be edited by the account that wrote them", 403, "", context);
|
|
995
|
+
const current = await atproto.getRecord(uri, signal);
|
|
996
|
+
const record = update(current.value);
|
|
997
|
+
await pdsCall("com.atproto.repo.putRecord", {
|
|
998
|
+
method: "POST",
|
|
999
|
+
json: {
|
|
1000
|
+
repo: viewer,
|
|
1001
|
+
collection: parsed.collection,
|
|
1002
|
+
rkey: parsed.rkey,
|
|
1003
|
+
record,
|
|
1004
|
+
swapRecord: current.cid
|
|
1005
|
+
},
|
|
1006
|
+
signal
|
|
1007
|
+
});
|
|
1008
|
+
return record;
|
|
1009
|
+
}
|
|
1010
|
+
async function deleteOwnRecord(uri, signal) {
|
|
1011
|
+
const parsed = parseAtUri(uri);
|
|
1012
|
+
const viewer = await viewerDid();
|
|
1013
|
+
if (!parsed || parsed.did !== viewer) throw new InsufficientScopeError("Records can only be deleted by the account that wrote them", 403, "", context);
|
|
1014
|
+
await pdsCall("com.atproto.repo.deleteRecord", {
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
json: {
|
|
1017
|
+
repo: viewer,
|
|
1018
|
+
collection: parsed.collection,
|
|
1019
|
+
rkey: parsed.rkey
|
|
1020
|
+
},
|
|
1021
|
+
signal
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
async function* ownRecords(collection, signal) {
|
|
1025
|
+
const repo = await viewerDid();
|
|
1026
|
+
let cursor;
|
|
1027
|
+
do {
|
|
1028
|
+
const data = await pdsCall("com.atproto.repo.listRecords", {
|
|
1029
|
+
query: {
|
|
1030
|
+
repo,
|
|
1031
|
+
collection,
|
|
1032
|
+
limit: 100,
|
|
1033
|
+
cursor
|
|
1034
|
+
},
|
|
1035
|
+
signal
|
|
1036
|
+
});
|
|
1037
|
+
yield* data.records;
|
|
1038
|
+
cursor = data.records.length ? data.cursor : void 0;
|
|
1039
|
+
} while (cursor);
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
writable,
|
|
1043
|
+
viewerDid,
|
|
1044
|
+
pdsCall,
|
|
1045
|
+
createRecord,
|
|
1046
|
+
putOwnRecord,
|
|
1047
|
+
deleteOwnRecord,
|
|
1048
|
+
ownRecords
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function rejectedPassword(error) {
|
|
1052
|
+
if (!(error instanceof AuthenticationRequiredError)) return error;
|
|
1053
|
+
const context = {
|
|
1054
|
+
forge: error.forge,
|
|
1055
|
+
instance: error.instance,
|
|
1056
|
+
url: error.url,
|
|
1057
|
+
method: error.method
|
|
1058
|
+
};
|
|
1059
|
+
return new TokenRevokedError("Identifier or app password was rejected by the PDS", error.status, error.body, context, { cause: error });
|
|
1060
|
+
}
|
|
1061
|
+
//#endregion
|
|
1062
|
+
//#region src/tangled/subscribe.ts
|
|
1063
|
+
function jetstreamUrl(base, collections, cursor) {
|
|
1064
|
+
const url = new URL(base);
|
|
1065
|
+
for (const collection of collections) url.searchParams.append("wantedCollections", collection);
|
|
1066
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
1067
|
+
return url.toString();
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Streams Jetstream commit events as normalised events. Messages are
|
|
1071
|
+
* translated strictly in arrival order, so a yielded cursor never skips an
|
|
1072
|
+
* earlier event.
|
|
1073
|
+
*/
|
|
1074
|
+
function subscribeJetstream(options) {
|
|
1075
|
+
return { [Symbol.asyncIterator]() {
|
|
1076
|
+
const queue = [];
|
|
1077
|
+
const waiters = [];
|
|
1078
|
+
let lastCursor = options.cursor;
|
|
1079
|
+
let finished = false;
|
|
1080
|
+
let chain = Promise.resolve();
|
|
1081
|
+
function settle(settled) {
|
|
1082
|
+
if (finished) return;
|
|
1083
|
+
if ("error" in settled || settled.done) finished = true;
|
|
1084
|
+
const waiter = waiters.shift();
|
|
1085
|
+
if (waiter) waiter(settled);
|
|
1086
|
+
else queue.push(settled);
|
|
1087
|
+
}
|
|
1088
|
+
const socket = options.createWebSocket(jetstreamUrl(options.url, options.collections, options.cursor));
|
|
1089
|
+
function stop() {
|
|
1090
|
+
if (!finished) settle({ done: true });
|
|
1091
|
+
socket.close(1e3);
|
|
1092
|
+
}
|
|
1093
|
+
if (options.signal?.aborted) stop();
|
|
1094
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
1095
|
+
socket.addEventListener("message", ({ data }) => {
|
|
1096
|
+
chain = chain.then(async () => {
|
|
1097
|
+
if (finished) return;
|
|
1098
|
+
const message = JSON.parse(String(data));
|
|
1099
|
+
const cursor = String(message.time_us);
|
|
1100
|
+
if (message.kind !== "commit") {
|
|
1101
|
+
lastCursor = cursor;
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const event = await options.toEvent(message);
|
|
1105
|
+
lastCursor = cursor;
|
|
1106
|
+
if (event) settle({
|
|
1107
|
+
done: false,
|
|
1108
|
+
value: {
|
|
1109
|
+
event,
|
|
1110
|
+
cursor
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}).catch((error) => {
|
|
1114
|
+
settle({ error: new SubscriptionClosedError("Failed to translate a subscription message", lastCursor, options.context, { cause: error }) });
|
|
1115
|
+
socket.close(1011);
|
|
1116
|
+
});
|
|
1117
|
+
});
|
|
1118
|
+
socket.addEventListener("error", (event) => {
|
|
1119
|
+
chain = chain.then(() => settle({ error: new SubscriptionClosedError("Subscription connection failed", lastCursor, options.context, { cause: event }) }));
|
|
1120
|
+
});
|
|
1121
|
+
socket.addEventListener("close", ({ code, reason }) => {
|
|
1122
|
+
chain = chain.then(() => settle({ error: new SubscriptionClosedError(`Subscription closed (${code ?? "no code"}${reason ? `: ${reason}` : ""})`, lastCursor, options.context) }));
|
|
1123
|
+
});
|
|
1124
|
+
return {
|
|
1125
|
+
next() {
|
|
1126
|
+
const settled = queue.shift();
|
|
1127
|
+
const resolve = (value) => {
|
|
1128
|
+
if ("error" in value) throw value.error;
|
|
1129
|
+
return value.done ? {
|
|
1130
|
+
done: true,
|
|
1131
|
+
value: void 0
|
|
1132
|
+
} : value;
|
|
1133
|
+
};
|
|
1134
|
+
if (settled) return Promise.resolve().then(() => resolve(settled));
|
|
1135
|
+
if (finished) return Promise.resolve({
|
|
1136
|
+
done: true,
|
|
1137
|
+
value: void 0
|
|
1138
|
+
});
|
|
1139
|
+
return new Promise((waiter) => waiters.push(waiter)).then(resolve);
|
|
1140
|
+
},
|
|
1141
|
+
return() {
|
|
1142
|
+
options.signal?.removeEventListener("abort", stop);
|
|
1143
|
+
stop();
|
|
1144
|
+
queue.length = 0;
|
|
1145
|
+
return Promise.resolve({
|
|
1146
|
+
done: true,
|
|
1147
|
+
value: void 0
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
} };
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/tangled/web.ts
|
|
1155
|
+
const SECTIONS = {
|
|
1156
|
+
issue: "issues",
|
|
1157
|
+
pull_request: "pulls"
|
|
1158
|
+
};
|
|
1159
|
+
const repoPath = (repo) => `/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}`;
|
|
1160
|
+
/** Tangled pages address threads by their display number, so parsed refs carry `displayNumber` and need resolving. */
|
|
1161
|
+
function tangledWeb(origin) {
|
|
1162
|
+
return {
|
|
1163
|
+
origin,
|
|
1164
|
+
repo: repoPath,
|
|
1165
|
+
thread: (ref) => SECTIONS[ref.kind] && ref.displayNumber ? `${repoPath(ref.repo)}/${SECTIONS[ref.kind]}/${encodeURIComponent(ref.displayNumber)}` : void 0,
|
|
1166
|
+
parse: (segments, _url, from) => {
|
|
1167
|
+
const [owner, name, section, id] = segments;
|
|
1168
|
+
if (!owner || !name) return;
|
|
1169
|
+
const repo = {
|
|
1170
|
+
...from,
|
|
1171
|
+
owner: owner.replace(/^@/, ""),
|
|
1172
|
+
name
|
|
1173
|
+
};
|
|
1174
|
+
const kind = section === "issues" ? "issue" : section === "pulls" ? "pull_request" : void 0;
|
|
1175
|
+
return kind && id ? {
|
|
1176
|
+
repo,
|
|
1177
|
+
thread: {
|
|
1178
|
+
...from,
|
|
1179
|
+
repo,
|
|
1180
|
+
kind,
|
|
1181
|
+
displayNumber: id
|
|
1182
|
+
}
|
|
1183
|
+
} : { repo };
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region src/tangled/webhooks.ts
|
|
1189
|
+
const SIGNATURE_HEADER = "x-tangled-signature-256";
|
|
1190
|
+
const EVENT_HEADER = "x-tangled-event";
|
|
1191
|
+
const PULL_ACTIONS = {
|
|
1192
|
+
created: "opened",
|
|
1193
|
+
resubmitted: "synchronised"
|
|
1194
|
+
};
|
|
1195
|
+
function verifyTangledSignature(delivery, secret) {
|
|
1196
|
+
return verifyHmacSignature(delivery, secret, {
|
|
1197
|
+
header: SIGNATURE_HEADER,
|
|
1198
|
+
prefix: "sha256="
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
/** A ref that did not exist before the push, or no longer exists after it. */
|
|
1202
|
+
const ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
1203
|
+
const PULL_KINDS = {
|
|
1204
|
+
created: "state_change",
|
|
1205
|
+
merged: "state_change",
|
|
1206
|
+
closed: "state_change",
|
|
1207
|
+
reopened: "state_change",
|
|
1208
|
+
resubmitted: "commit"
|
|
1209
|
+
};
|
|
1210
|
+
function translateTangledWebhook(instance, delivery, webUrl) {
|
|
1211
|
+
const event = headerValue(delivery.headers, EVENT_HEADER);
|
|
1212
|
+
if (!event) throw new WebhookVerificationError(`Missing ${EVENT_HEADER} header`, {
|
|
1213
|
+
forge: FORGE,
|
|
1214
|
+
instance
|
|
1215
|
+
});
|
|
1216
|
+
const deliveryId = headerValue(delivery.headers, "x-tangled-delivery") ?? crypto.randomUUID();
|
|
1217
|
+
const payload = JSON.parse(bodyText(delivery.body));
|
|
1218
|
+
const ownerDid = payload.repository?.owner?.did ?? payload.repository?.full_name?.split("/")[0];
|
|
1219
|
+
const repo = payload.repository && ownerDid ? {
|
|
1220
|
+
forge: FORGE,
|
|
1221
|
+
instance,
|
|
1222
|
+
owner: ownerDid,
|
|
1223
|
+
name: payload.repository.name
|
|
1224
|
+
} : void 0;
|
|
1225
|
+
const actorDid = payload.sender?.did ?? payload.pusher?.did;
|
|
1226
|
+
const actor = actorDid ? {
|
|
1227
|
+
forge: FORGE,
|
|
1228
|
+
instance,
|
|
1229
|
+
login: actorDid,
|
|
1230
|
+
id: actorDid,
|
|
1231
|
+
url: `${webUrl}/${actorDid}`,
|
|
1232
|
+
isBotHint: false
|
|
1233
|
+
} : void 0;
|
|
1234
|
+
const who = actor?.login ?? "someone";
|
|
1235
|
+
if (event.startsWith("pull_request:") && payload.pull_request) {
|
|
1236
|
+
const action = payload.action ?? event.slice(13);
|
|
1237
|
+
return [{
|
|
1238
|
+
forge: FORGE,
|
|
1239
|
+
instance,
|
|
1240
|
+
id: deliveryId,
|
|
1241
|
+
kind: PULL_KINDS[action] ?? "other",
|
|
1242
|
+
kindRaw: event,
|
|
1243
|
+
action: PULL_ACTIONS[action] ?? eventAction(action),
|
|
1244
|
+
actionRaw: action,
|
|
1245
|
+
summary: `${who} ${action === "resubmitted" ? "pushed a new round to" : action} pull request #${payload.pull_request.number}`,
|
|
1246
|
+
occurredAt: action === "created" ? toDate(payload.pull_request.created_at) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date(),
|
|
1247
|
+
actor,
|
|
1248
|
+
repo,
|
|
1249
|
+
thread: repo ? {
|
|
1250
|
+
forge: FORGE,
|
|
1251
|
+
instance,
|
|
1252
|
+
repo,
|
|
1253
|
+
kind: "pull_request",
|
|
1254
|
+
typeRaw: "sh.tangled.repo.pull",
|
|
1255
|
+
displayNumber: String(payload.pull_request.number)
|
|
1256
|
+
} : void 0,
|
|
1257
|
+
source: "webhook",
|
|
1258
|
+
payload
|
|
1259
|
+
}];
|
|
1260
|
+
}
|
|
1261
|
+
const base = {
|
|
1262
|
+
forge: FORGE,
|
|
1263
|
+
instance,
|
|
1264
|
+
id: deliveryId,
|
|
1265
|
+
kindRaw: event,
|
|
1266
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1267
|
+
actor,
|
|
1268
|
+
repo,
|
|
1269
|
+
source: "webhook",
|
|
1270
|
+
payload
|
|
1271
|
+
};
|
|
1272
|
+
if (event === "push" && payload.ref) return [{
|
|
1273
|
+
...base,
|
|
1274
|
+
...refEvent(who, {
|
|
1275
|
+
ref: payload.ref,
|
|
1276
|
+
before: payload.before,
|
|
1277
|
+
after: payload.after,
|
|
1278
|
+
created: payload.before === ZERO_SHA,
|
|
1279
|
+
deleted: payload.after === ZERO_SHA,
|
|
1280
|
+
commits: (payload.commits ?? []).map((commit) => ({
|
|
1281
|
+
sha: commit.sha ?? commit.id ?? "",
|
|
1282
|
+
message: commit.message,
|
|
1283
|
+
author: commit.author?.did ?? commit.author?.name
|
|
1284
|
+
}))
|
|
1285
|
+
})
|
|
1286
|
+
}];
|
|
1287
|
+
return [{
|
|
1288
|
+
...base,
|
|
1289
|
+
kind: "other",
|
|
1290
|
+
summary: `${who} ${event}`
|
|
1291
|
+
}];
|
|
1292
|
+
}
|
|
1293
|
+
const TANGLED_WEBHOOK_EVENTS = [
|
|
1294
|
+
{
|
|
1295
|
+
kind: "comment",
|
|
1296
|
+
action: "created"
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
kind: "state_change",
|
|
1300
|
+
action: "opened"
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
kind: "state_change",
|
|
1304
|
+
action: "closed"
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
kind: "state_change",
|
|
1308
|
+
action: "merged"
|
|
1309
|
+
},
|
|
1310
|
+
{ kind: "push" }
|
|
1311
|
+
];
|
|
1312
|
+
const tangledWebhooks = ({ options, instance, baseUrl }) => ({
|
|
1313
|
+
events: TANGLED_WEBHOOK_EVENTS,
|
|
1314
|
+
verify: (delivery) => verifyTangledSignature(delivery, options.webhookSecret),
|
|
1315
|
+
translate: async (delivery) => {
|
|
1316
|
+
const events = await translateTangledWebhook(instance, delivery, baseUrl);
|
|
1317
|
+
if (!options.resolveDisplayNumber) return events;
|
|
1318
|
+
for (const event of events) {
|
|
1319
|
+
const thread = event.thread;
|
|
1320
|
+
if (thread?.displayNumber && !thread.number) {
|
|
1321
|
+
const number = await options.resolveDisplayNumber(thread.repo, thread.kind, thread.displayNumber);
|
|
1322
|
+
if (number) event.thread = {
|
|
1323
|
+
...thread,
|
|
1324
|
+
number,
|
|
1325
|
+
externalId: number
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return events;
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region src/tangled/index.ts
|
|
1334
|
+
const ISSUE_AND_PULL = {
|
|
1335
|
+
issue: true,
|
|
1336
|
+
pull_request: true
|
|
1337
|
+
};
|
|
1338
|
+
const TANGLED = {
|
|
1339
|
+
forge: FORGE,
|
|
1340
|
+
experimental: true,
|
|
1341
|
+
baseUrl: "https://tangled.org",
|
|
1342
|
+
headers: { accept: "application/json" },
|
|
1343
|
+
setup({ options, instance, baseUrl: webUrl, origin: context, fetcher, createFetcher: fetcherFor }) {
|
|
1344
|
+
const atproto = createAtprotoClient({
|
|
1345
|
+
fetcher,
|
|
1346
|
+
plcUrl: (options.plcUrl ?? "https://plc.directory").replace(/\/$/, ""),
|
|
1347
|
+
backlinksUrl: (options.backlinksUrl ?? "https://constellation.microcosm.blue").replace(/\/$/, ""),
|
|
1348
|
+
handleResolverUrl: (options.handleResolverUrl ?? "https://public.api.bsky.app").replace(/\/$/, ""),
|
|
1349
|
+
recordsUrl: options.recordsUrl?.replace(/\/$/, ""),
|
|
1350
|
+
context
|
|
1351
|
+
});
|
|
1352
|
+
const api = fetcherFor({ baseUrl: (options.apiUrl ?? "https://api.tangled.org").replace(/\/$/, "") });
|
|
1353
|
+
const { writable, viewerDid, pdsCall, createRecord, putOwnRecord, deleteOwnRecord, ownRecords } = createTangledSession({
|
|
1354
|
+
options,
|
|
1355
|
+
fetcher,
|
|
1356
|
+
atproto,
|
|
1357
|
+
context
|
|
1358
|
+
});
|
|
1359
|
+
/** The repo DID a thread record must reference, from the ref or the repo record. */
|
|
1360
|
+
const { stateAuthors, actorFor, resolveRepo, threadFor, subjectUri, activity, toStreamEvent, canonicalRepo, canonicalRef, repoDidOf, toRepo, readThread, wholePage, listPage, listComments, findSubscription } = createTangledRecords({
|
|
1361
|
+
options,
|
|
1362
|
+
instance,
|
|
1363
|
+
webUrl,
|
|
1364
|
+
context,
|
|
1365
|
+
atproto,
|
|
1366
|
+
api,
|
|
1367
|
+
fetcher,
|
|
1368
|
+
ownRecords
|
|
1369
|
+
});
|
|
1370
|
+
const threads = {
|
|
1371
|
+
get: perKind(ISSUE_AND_PULL, readThread),
|
|
1372
|
+
getMany: verb(true, (refs, options) => getManyConcurrently(refs, readThread, options)),
|
|
1373
|
+
listPage: perKind({
|
|
1374
|
+
issue: true,
|
|
1375
|
+
pull_request: true
|
|
1376
|
+
}, listPage),
|
|
1377
|
+
eventsPage: verb(true, async (thread, listOptions = {}) => {
|
|
1378
|
+
const { ref, uri, kind } = subjectUri(thread);
|
|
1379
|
+
const target = atUri(uri.did, uri.collection, uri.rkey);
|
|
1380
|
+
return wholePage(forgeIterable(async function* (warn) {
|
|
1381
|
+
const events = [];
|
|
1382
|
+
for await (const item of activity(target, kind, await stateAuthors(uri.did, ref.repo, warn, listOptions.signal), warn, listOptions)) {
|
|
1383
|
+
const author = parseAtUri(item.uri).did;
|
|
1384
|
+
events.push(toActivityEvent(instance, {
|
|
1385
|
+
...item,
|
|
1386
|
+
operation: "create"
|
|
1387
|
+
}, await actorFor(author, listOptions.signal), ref, "poll", /* @__PURE__ */ new Date(0)));
|
|
1388
|
+
}
|
|
1389
|
+
yield* events.sort((a, b) => a.occurredAt.getTime() - b.occurredAt.getTime());
|
|
1390
|
+
}));
|
|
1391
|
+
}),
|
|
1392
|
+
commentsPage: perKind(ISSUE_AND_PULL, (thread, listOptions) => wholePage(listComments(thread, listOptions)))
|
|
1393
|
+
};
|
|
1394
|
+
const comment = async (thread, body, options) => {
|
|
1395
|
+
const { ref, uri } = subjectUri(thread);
|
|
1396
|
+
const target = atUri(uri.did, uri.collection, uri.rkey);
|
|
1397
|
+
const { cid } = await atproto.getRecord(target, options?.signal);
|
|
1398
|
+
const record = {
|
|
1399
|
+
body: {
|
|
1400
|
+
$type: "sh.tangled.markup.markdown",
|
|
1401
|
+
text: body,
|
|
1402
|
+
original: body
|
|
1403
|
+
},
|
|
1404
|
+
subject: {
|
|
1405
|
+
uri: target,
|
|
1406
|
+
cid: cid ?? ""
|
|
1407
|
+
},
|
|
1408
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1409
|
+
};
|
|
1410
|
+
return toRecordComment(ref, (await createRecord(COLLECTIONS.comment, { ...record }, options?.signal)).uri, record, await actorFor(await viewerDid(), options?.signal));
|
|
1411
|
+
};
|
|
1412
|
+
const editComment = async (ref, body, options) => {
|
|
1413
|
+
const record = await putOwnRecord(ref.id, (current) => ({
|
|
1414
|
+
...current,
|
|
1415
|
+
body: typeof current.body === "string" ? body : {
|
|
1416
|
+
$type: "sh.tangled.markup.markdown",
|
|
1417
|
+
text: body,
|
|
1418
|
+
original: body
|
|
1419
|
+
}
|
|
1420
|
+
}), options?.signal);
|
|
1421
|
+
return toRecordComment(ref.thread, ref.id, record, await actorFor(await viewerDid(), options?.signal));
|
|
1422
|
+
};
|
|
1423
|
+
const deleteComment = (ref, options) => deleteOwnRecord(ref.id, options?.signal);
|
|
1424
|
+
const create = async (repo, input, options) => {
|
|
1425
|
+
if (input.kind !== "issue") throw new UnsupportedOperationError("Tangled pull requests are patches; only issues can be created", context);
|
|
1426
|
+
if (input.labels?.length || input.assignees?.length) throw new UnsupportedOperationError("Tangled labels and assignees are not written yet", context);
|
|
1427
|
+
const [address, repoDid] = await Promise.all([canonicalRef(repo, options?.signal), repoDidOf(repo, options?.signal)]);
|
|
1428
|
+
const record = {
|
|
1429
|
+
repo: repoDid,
|
|
1430
|
+
title: input.title,
|
|
1431
|
+
body: input.body,
|
|
1432
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1433
|
+
};
|
|
1434
|
+
const created = await createRecord(COLLECTIONS.issue, { ...record }, options?.signal);
|
|
1435
|
+
return toThread(toThreadRef(instance, {
|
|
1436
|
+
...address,
|
|
1437
|
+
externalId: repoDid
|
|
1438
|
+
}, parseAtUri(created.uri)), record, await actorFor(await viewerDid(), options?.signal), {}, 0);
|
|
1439
|
+
};
|
|
1440
|
+
const update = async (thread, input, options) => {
|
|
1441
|
+
const { ref, uri } = subjectUri(thread);
|
|
1442
|
+
return toThread(ref, await putOwnRecord(atUri(uri.did, uri.collection, uri.rkey), (current) => ({
|
|
1443
|
+
...current,
|
|
1444
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
1445
|
+
...input.body === void 0 ? {} : { body: input.body }
|
|
1446
|
+
}), options?.signal), await actorFor(uri.did, options?.signal), {}, 0);
|
|
1447
|
+
};
|
|
1448
|
+
const subscription = async (thread, options) => await findSubscription(thread, options?.signal) ? "subscribed" : "none";
|
|
1449
|
+
const subscribe = async (thread, options) => {
|
|
1450
|
+
if (await findSubscription(thread, options?.signal)) return;
|
|
1451
|
+
const { uri } = subjectUri(thread);
|
|
1452
|
+
await createRecord(COLLECTIONS.subscription, {
|
|
1453
|
+
subject: {
|
|
1454
|
+
$type: `${COLLECTIONS.subscription}#uri`,
|
|
1455
|
+
uri: atUri(uri.did, uri.collection, uri.rkey)
|
|
1456
|
+
},
|
|
1457
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1458
|
+
}, options?.signal);
|
|
1459
|
+
};
|
|
1460
|
+
const unsubscribe = async (thread, options) => {
|
|
1461
|
+
const existing = await findSubscription(thread, options?.signal);
|
|
1462
|
+
if (existing) await deleteOwnRecord(existing, options?.signal);
|
|
1463
|
+
};
|
|
1464
|
+
const setState = async (thread, state, signal) => {
|
|
1465
|
+
const { uri, kind } = subjectUri(thread);
|
|
1466
|
+
const target = atUri(uri.did, uri.collection, uri.rkey);
|
|
1467
|
+
const { ref } = await threadFor(target, signal);
|
|
1468
|
+
const [viewer, allowed] = await Promise.all([viewerDid(), stateAuthors(uri.did, ref.repo, void 0, signal)]);
|
|
1469
|
+
if (!allowed.has(viewer)) throw new InsufficientScopeError("Only the author, the repo owner or a collaborator can change this state; the appview would ignore the record", 403, "", context);
|
|
1470
|
+
await createRecord(STATE_COLLECTION[kind].collection, kind === "issue" ? {
|
|
1471
|
+
issue: target,
|
|
1472
|
+
state: `${COLLECTIONS.issueState}.${state}`,
|
|
1473
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1474
|
+
} : {
|
|
1475
|
+
pull: target,
|
|
1476
|
+
status: `${COLLECTIONS.pullStatus}.${state}`,
|
|
1477
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1478
|
+
}, signal);
|
|
1479
|
+
};
|
|
1480
|
+
const close = (thread, options) => setState(thread, "closed", options?.signal);
|
|
1481
|
+
const reopen = (thread, options) => setState(thread, "open", options?.signal);
|
|
1482
|
+
const notificationsUrl = options.notificationsUrl?.replace(/\/$/, "");
|
|
1483
|
+
const notifications = writable && notificationsUrl ? createTangledNotifications(notificationsUrl, {
|
|
1484
|
+
instance,
|
|
1485
|
+
context,
|
|
1486
|
+
fetcher,
|
|
1487
|
+
atproto,
|
|
1488
|
+
pdsCall,
|
|
1489
|
+
resolveRepo,
|
|
1490
|
+
actorFor
|
|
1491
|
+
}) : void 0;
|
|
1492
|
+
const createWebSocket = options.webSocket ?? ((url) => new globalThis.WebSocket(url));
|
|
1493
|
+
const signedIn = {
|
|
1494
|
+
issue: writable,
|
|
1495
|
+
pull_request: writable
|
|
1496
|
+
};
|
|
1497
|
+
return {
|
|
1498
|
+
traits: {
|
|
1499
|
+
eventKinds: "native",
|
|
1500
|
+
authKinds: [
|
|
1501
|
+
"anonymous",
|
|
1502
|
+
"app_password",
|
|
1503
|
+
"oauth"
|
|
1504
|
+
]
|
|
1505
|
+
},
|
|
1506
|
+
request: api,
|
|
1507
|
+
repos: {
|
|
1508
|
+
get: verb(true, async (ref, options) => {
|
|
1509
|
+
const { ref: found, record } = await canonicalRepo(ref, options?.signal);
|
|
1510
|
+
return toRepo(found, record.value, record);
|
|
1511
|
+
}),
|
|
1512
|
+
listPage: verb(writable, async (listOptions = {}) => {
|
|
1513
|
+
if (!writable) throw new UnsupportedOperationError("Listing your repositories needs auth", context);
|
|
1514
|
+
return wholePage(forgeIterable(async function* () {
|
|
1515
|
+
const owner = await viewerDid();
|
|
1516
|
+
for await (const record of ownRecords(COLLECTIONS.repo, listOptions.signal)) {
|
|
1517
|
+
const rkey = parseAtUri(record.uri).rkey;
|
|
1518
|
+
yield toRepo({
|
|
1519
|
+
forge: FORGE,
|
|
1520
|
+
instance,
|
|
1521
|
+
owner,
|
|
1522
|
+
name: rkey
|
|
1523
|
+
}, record.value, record);
|
|
1524
|
+
}
|
|
1525
|
+
}));
|
|
1526
|
+
})
|
|
1527
|
+
},
|
|
1528
|
+
notifications,
|
|
1529
|
+
users: { me: verb(writable, async (options) => ({
|
|
1530
|
+
...await actorFor(await viewerDid(), options?.signal),
|
|
1531
|
+
raw: void 0
|
|
1532
|
+
})) },
|
|
1533
|
+
threads: {
|
|
1534
|
+
...threads,
|
|
1535
|
+
comment: perKind(signedIn, comment),
|
|
1536
|
+
editComment: perKind(signedIn, editComment),
|
|
1537
|
+
deleteComment: perKind(signedIn, deleteComment),
|
|
1538
|
+
create: perKind({ issue: writable }, create),
|
|
1539
|
+
update: perKind(signedIn, update),
|
|
1540
|
+
close: perKind(signedIn, close),
|
|
1541
|
+
reopen: perKind(signedIn, reopen),
|
|
1542
|
+
subscriptions: perKind(signedIn, {
|
|
1543
|
+
subscription,
|
|
1544
|
+
subscribe,
|
|
1545
|
+
unsubscribe
|
|
1546
|
+
})
|
|
1547
|
+
},
|
|
1548
|
+
sources: { subscribe: verb(true, (subscribeOptions) => subscribeJetstream({
|
|
1549
|
+
...subscribeOptions,
|
|
1550
|
+
url: options.jetstreamUrl ?? "wss://jetstream2.us-east.bsky.network/subscribe",
|
|
1551
|
+
collections: THREAD_COLLECTIONS,
|
|
1552
|
+
createWebSocket,
|
|
1553
|
+
toEvent: toStreamEvent,
|
|
1554
|
+
context
|
|
1555
|
+
})) },
|
|
1556
|
+
web: tangledWeb(webUrl),
|
|
1557
|
+
scopes: () => ({ note: "An atproto app password or OAuth session; Tangled has no per-verb scopes" }),
|
|
1558
|
+
webhooks: {}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
/** Creates a Tangled provider. Records are read from PDSes; pushes arrive through Jetstream. */
|
|
1563
|
+
const tangled = /* @__PURE__ */ defineForgeProvider({
|
|
1564
|
+
...TANGLED,
|
|
1565
|
+
webhooks: tangledWebhooks
|
|
1566
|
+
});
|
|
1567
|
+
/** `tangled()` without webhook ingestion, for bundles that never receive a delivery. */
|
|
1568
|
+
const tangledLite = /* @__PURE__ */ defineForgeProvider(TANGLED);
|
|
1569
|
+
//#endregion
|
|
1570
|
+
export { tangledLite as n, tangled as t };
|