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.
Files changed (152) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +72 -0
  3. package/dist/azure-devops/index.d.mts +2 -0
  4. package/dist/azure-devops/index.mjs +1 -0
  5. package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
  6. package/dist/bitbucket/index.d.mts +2 -0
  7. package/dist/bitbucket/index.mjs +1 -0
  8. package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
  9. package/dist/crypto-CQNEyYHD.mjs +156 -0
  10. package/dist/cursor-origin/index.d.mts +2 -0
  11. package/dist/cursor-origin/index.mjs +1 -0
  12. package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
  13. package/dist/define-B2_4ob2i.d.mts +307 -0
  14. package/dist/define-D3yEXtKK.mjs +2276 -0
  15. package/dist/env.d.mts +46 -0
  16. package/dist/env.mjs +283 -0
  17. package/dist/fake/index.d.mts +227 -0
  18. package/dist/fake/index.mjs +1001 -0
  19. package/dist/fetch-D4pSRvvA.d.mts +1723 -0
  20. package/dist/forgejo/index.d.mts +2 -0
  21. package/dist/forgejo/index.mjs +1 -0
  22. package/dist/forgejo-BCefbHUN.mjs +2005 -0
  23. package/dist/gitea/index.d.mts +2 -0
  24. package/dist/gitea/index.mjs +22 -0
  25. package/dist/gitee/index.d.mts +2 -0
  26. package/dist/gitee/index.mjs +1 -0
  27. package/dist/gitee-CA-f7Rs8.mjs +1272 -0
  28. package/dist/github/index.d.mts +2 -0
  29. package/dist/github/index.mjs +1 -0
  30. package/dist/github-B0umL8_f.mjs +3259 -0
  31. package/dist/gitlab/index.d.mts +2 -0
  32. package/dist/gitlab/index.mjs +1 -0
  33. package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
  34. package/dist/graphql-D7uGzZTf.mjs +366 -0
  35. package/dist/index-BQ6VS31s.d.mts +32 -0
  36. package/dist/index-C-jz9Mo3.d.mts +16 -0
  37. package/dist/index-C4fYzmK3.d.mts +102 -0
  38. package/dist/index-CQiIX4SH.d.mts +27 -0
  39. package/dist/index-CTj5gQzl.d.mts +35 -0
  40. package/dist/index-CliBWzOF.d.mts +23 -0
  41. package/dist/index-Cz_y-t3d.d.mts +36 -0
  42. package/dist/index-DEt25E1w.d.mts +77 -0
  43. package/dist/index-DIuS0EOy.d.mts +43 -0
  44. package/dist/index-uSpsMCrC.d.mts +26 -0
  45. package/dist/index.d.mts +41 -0
  46. package/dist/index.mjs +13 -0
  47. package/dist/kit.d.mts +183 -0
  48. package/dist/kit.mjs +4 -0
  49. package/dist/provider-CoZ7cCxp.d.mts +1439 -0
  50. package/dist/provider-Ed1YhkQN.mjs +169 -0
  51. package/dist/pushin/index.d.mts +2 -0
  52. package/dist/pushin/index.mjs +1 -0
  53. package/dist/pushin-CkxgJ2cU.mjs +343 -0
  54. package/dist/schema/Actor.json +96 -0
  55. package/dist/schema/AlertSupport.json +49 -0
  56. package/dist/schema/AuthKind.json +15 -0
  57. package/dist/schema/Branch.json +31 -0
  58. package/dist/schema/ChangedFile.json +58 -0
  59. package/dist/schema/Check.json +169 -0
  60. package/dist/schema/CheckRef.json +114 -0
  61. package/dist/schema/CheckState.json +14 -0
  62. package/dist/schema/ChecksSummary.json +34 -0
  63. package/dist/schema/CiJob.json +186 -0
  64. package/dist/schema/CiJobRef.json +132 -0
  65. package/dist/schema/CiRun.json +234 -0
  66. package/dist/schema/CiRunRef.json +103 -0
  67. package/dist/schema/CodeFragment.json +19 -0
  68. package/dist/schema/CodeMatch.json +128 -0
  69. package/dist/schema/Collaborator.json +169 -0
  70. package/dist/schema/Comment.json +336 -0
  71. package/dist/schema/CommentRef.json +158 -0
  72. package/dist/schema/Commit.json +307 -0
  73. package/dist/schema/CommitRef.json +104 -0
  74. package/dist/schema/CommitSignature.json +118 -0
  75. package/dist/schema/Comparison.json +355 -0
  76. package/dist/schema/Cursor.json +21 -0
  77. package/dist/schema/EventAction.json +33 -0
  78. package/dist/schema/EventDetail.json +649 -0
  79. package/dist/schema/EventKind.json +26 -0
  80. package/dist/schema/FileContent.json +73 -0
  81. package/dist/schema/FileStatus.json +15 -0
  82. package/dist/schema/ForgeCapabilities.json +737 -0
  83. package/dist/schema/ForgeEvent.json +789 -0
  84. package/dist/schema/ForgeInstance.json +7 -0
  85. package/dist/schema/ForgeKind.json +32 -0
  86. package/dist/schema/ForgeWarning.json +49 -0
  87. package/dist/schema/ForgeWarningCode.json +7 -0
  88. package/dist/schema/GetManyResult.json +699 -0
  89. package/dist/schema/Installation.json +131 -0
  90. package/dist/schema/InstallationToken.json +31 -0
  91. package/dist/schema/KnownForgeKind.json +19 -0
  92. package/dist/schema/Label.json +22 -0
  93. package/dist/schema/MergeMethod.json +14 -0
  94. package/dist/schema/Milestone.json +40 -0
  95. package/dist/schema/Notification.json +498 -0
  96. package/dist/schema/NotificationReason.json +28 -0
  97. package/dist/schema/NotificationRef.json +60 -0
  98. package/dist/schema/NotificationSubject.json +376 -0
  99. package/dist/schema/PerKind.json +49 -0
  100. package/dist/schema/PullBranches.json +125 -0
  101. package/dist/schema/PushCommit.json +24 -0
  102. package/dist/schema/Reaction.json +148 -0
  103. package/dist/schema/ReactionContent.json +17 -0
  104. package/dist/schema/ReactionSummary.json +70 -0
  105. package/dist/schema/Release.json +286 -0
  106. package/dist/schema/ReleaseAsset.json +169 -0
  107. package/dist/schema/ReleaseAssetRef.json +138 -0
  108. package/dist/schema/ReleaseRef.json +107 -0
  109. package/dist/schema/Repo.json +354 -0
  110. package/dist/schema/RepoFeatures.json +35 -0
  111. package/dist/schema/RepoPermissions.json +35 -0
  112. package/dist/schema/RepoRef.json +77 -0
  113. package/dist/schema/RepoRole.json +15 -0
  114. package/dist/schema/RepoVisibility.json +12 -0
  115. package/dist/schema/Review.json +371 -0
  116. package/dist/schema/ReviewComment.json +280 -0
  117. package/dist/schema/ReviewCommentInput.json +34 -0
  118. package/dist/schema/ReviewRef.json +158 -0
  119. package/dist/schema/ReviewState.json +15 -0
  120. package/dist/schema/Reviewer.json +133 -0
  121. package/dist/schema/SecurityAlert.json +219 -0
  122. package/dist/schema/SecurityAlertKind.json +14 -0
  123. package/dist/schema/SecurityAlertRef.json +122 -0
  124. package/dist/schema/SecuritySeverity.json +14 -0
  125. package/dist/schema/SourceKind.json +12 -0
  126. package/dist/schema/SubscriptionState.json +12 -0
  127. package/dist/schema/Support.json +23 -0
  128. package/dist/schema/Tag.json +25 -0
  129. package/dist/schema/TextLimits.json +18 -0
  130. package/dist/schema/Thread.json +653 -0
  131. package/dist/schema/ThreadKind.json +14 -0
  132. package/dist/schema/ThreadRef.json +131 -0
  133. package/dist/schema/ThreadStack.json +146 -0
  134. package/dist/schema/ThreadState.json +13 -0
  135. package/dist/schema/TreeEntry.json +46 -0
  136. package/dist/schema/TreeEntryType.json +13 -0
  137. package/dist/schema/UpsertCommentResult.json +355 -0
  138. package/dist/schema/User.json +123 -0
  139. package/dist/schema/Webhook.json +188 -0
  140. package/dist/schema/WebhookDeliveryRecord.json +173 -0
  141. package/dist/schema/WebhookDeliveryRef.json +135 -0
  142. package/dist/schema/WebhookRef.json +105 -0
  143. package/dist/schema/index.d.mts +4 -0
  144. package/dist/schema/index.mjs +3219 -0
  145. package/dist/tangled/index.d.mts +2 -0
  146. package/dist/tangled/index.mjs +1 -0
  147. package/dist/tangled-ChdPmiv-.mjs +1570 -0
  148. package/dist/testing/index.d.mts +123 -0
  149. package/dist/testing/index.mjs +242 -0
  150. package/dist/webhooks-rn9-8lxT.mjs +70 -0
  151. package/package.json +115 -3
  152. 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 };