@v7-cms/adapters 0.0.0-alpha.9

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.
@@ -0,0 +1,339 @@
1
+ /**
2
+ * The GitHub backend.
3
+ *
4
+ * Reads and writes repository contents through the REST API. Two things shape the design:
5
+ *
6
+ * - Every write carries the sha it was based on, so a change made elsewhere in the meantime is
7
+ * refused rather than silently overwritten. `ConflictError` is what the editor reacts to.
8
+ * - Listing uses the trees API once per directory tree and caches it, instead of one request per
9
+ * file, because a content folder with a few hundred entries would otherwise cost a few hundred
10
+ * requests against a rate limit.
11
+ */
12
+ import { ConflictError, NotFoundError, } from '@v7-cms/core/storage';
13
+ const enc = (path) => path
14
+ .split('/')
15
+ .map((segment) => encodeURIComponent(segment))
16
+ .join('/');
17
+ export class GitHubAdapter {
18
+ kind = 'github';
19
+ owner;
20
+ repo;
21
+ apiRoot;
22
+ request;
23
+ token;
24
+ branch;
25
+ via;
26
+ /** Cached after the first lookup, so the shell does not re-request on every render. */
27
+ accountInfo;
28
+ accountFetched = false;
29
+ author;
30
+ /** Path -> blob sha, filled from one tree request per branch. */
31
+ tree = null;
32
+ dirCache = new Map();
33
+ constructor(options) {
34
+ this.owner = options.owner;
35
+ this.repo = options.repo;
36
+ this.branch = options.branch ?? 'main';
37
+ this.token = options.token;
38
+ this.apiRoot = (options.apiRoot ?? 'https://api.github.com').replace(/\/$/, '');
39
+ // `fetch` must keep its receiver. Reading it off `globalThis` and calling it later as a bare
40
+ // function loses the binding, and the browser throws "Illegal invocation" — which is what
41
+ // happens the moment a real page (rather than a test with an injected stub) lists a directory.
42
+ const base = options.fetch ?? globalThis.fetch;
43
+ this.request = base === globalThis.fetch ? base.bind(globalThis) : base;
44
+ this.author = options.author;
45
+ this.via = options.via ?? 'token';
46
+ }
47
+ async init() {
48
+ // Fail early and clearly rather than on the first write.
49
+ const response = await this.call(`/repos/${this.owner}/${this.repo}`);
50
+ if (!response.ok)
51
+ throw new Error(await describe(response, 'open the repository'));
52
+ }
53
+ /**
54
+ * The authenticated user, fetched once.
55
+ *
56
+ * The token alone says nothing about who holds it, and an author who signed in on a shared
57
+ * machine needs to see that. `init()` already proved the token works, so a failure here is not
58
+ * worth failing the editor over — it returns undefined and the panel falls back to the repo.
59
+ */
60
+ async account() {
61
+ if (this.accountFetched)
62
+ return this.accountInfo;
63
+ this.accountFetched = true;
64
+ try {
65
+ const response = await this.call('/user');
66
+ if (!response.ok)
67
+ return undefined;
68
+ const user = (await response.json());
69
+ this.accountInfo = {
70
+ ...(user.login ? { login: user.login } : {}),
71
+ ...(user.name ? { name: user.name } : {}),
72
+ ...(user.avatar_url ? { avatar: user.avatar_url } : {}),
73
+ via: this.via,
74
+ repo: { owner: this.owner, repo: this.repo, branch: this.branch },
75
+ };
76
+ return this.accountInfo;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ /** Swap the token after a sign-in, so the same adapter can outlive a session change. */
83
+ setToken(token) {
84
+ this.token = token;
85
+ this.accountInfo = undefined;
86
+ this.accountFetched = false;
87
+ this.tree = null;
88
+ this.dirCache.clear();
89
+ }
90
+ setBranch(branch) {
91
+ this.branch = branch;
92
+ this.tree = null;
93
+ this.dirCache.clear();
94
+ }
95
+ async call(path, init = {}) {
96
+ return this.request(`${this.apiRoot}${path}`, {
97
+ ...init,
98
+ headers: {
99
+ Authorization: `Bearer ${this.token}`,
100
+ Accept: 'application/vnd.github+json',
101
+ 'X-GitHub-Api-Version': '2022-11-28',
102
+ ...(init.body ? { 'Content-Type': 'application/json' } : {}),
103
+ ...init.headers,
104
+ },
105
+ });
106
+ }
107
+ /**
108
+ * One request for the whole branch, then everything is served from it. Cleared on every write,
109
+ * so a stale listing is never shown after a change.
110
+ */
111
+ async loadTree() {
112
+ if (this.tree)
113
+ return this.tree;
114
+ const response = await this.call(`/repos/${this.owner}/${this.repo}/git/trees/${encodeURIComponent(this.branch)}?recursive=1`);
115
+ if (response.status === 404)
116
+ throw new NotFoundError(`branch ${this.branch}`);
117
+ if (!response.ok)
118
+ throw new Error(await describe(response, 'list the repository'));
119
+ const body = (await response.json());
120
+ const map = new Map();
121
+ for (const entry of body.tree ?? [])
122
+ if (entry.type === 'blob')
123
+ map.set(entry.path, entry.sha);
124
+ this.tree = map;
125
+ return map;
126
+ }
127
+ forget() {
128
+ this.tree = null;
129
+ this.dirCache.clear();
130
+ }
131
+ async listDir(path) {
132
+ const cached = this.dirCache.get(path);
133
+ if (cached)
134
+ return cached;
135
+ const tree = await this.loadTree();
136
+ const prefix = path ? `${path.replace(/\/+$/, '')}/` : '';
137
+ const seen = new Map();
138
+ for (const [filePath, sha] of tree) {
139
+ if (!filePath.startsWith(prefix))
140
+ continue;
141
+ const rest = filePath.slice(prefix.length);
142
+ const slash = rest.indexOf('/');
143
+ if (slash === -1)
144
+ seen.set(rest, { name: rest, type: 'file', sha });
145
+ else {
146
+ const dir = rest.slice(0, slash);
147
+ if (!seen.has(dir))
148
+ seen.set(dir, { name: dir, type: 'dir' });
149
+ }
150
+ }
151
+ const entries = [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
152
+ this.dirCache.set(path, entries);
153
+ return entries;
154
+ }
155
+ async exists(path) {
156
+ return (await this.loadTree()).has(path);
157
+ }
158
+ async readFile(path) {
159
+ const response = await this.call(`/repos/${this.owner}/${this.repo}/contents/${enc(path)}?ref=${encodeURIComponent(this.branch)}`);
160
+ if (response.status === 404)
161
+ throw new NotFoundError(path);
162
+ if (!response.ok)
163
+ throw new Error(await describe(response, `read ${path}`));
164
+ const body = (await response.json());
165
+ if (body.encoding !== 'base64' || typeof body.content !== 'string') {
166
+ throw new Error(`${path} is not a file this editor can read.`);
167
+ }
168
+ return { text: decodeBase64(body.content), sha: body.sha };
169
+ }
170
+ async readBinary(path) {
171
+ const response = await this.call(`/repos/${this.owner}/${this.repo}/contents/${enc(path)}?ref=${encodeURIComponent(this.branch)}`);
172
+ if (response.status === 404)
173
+ throw new NotFoundError(path);
174
+ if (!response.ok)
175
+ throw new Error(await describe(response, `read ${path}`));
176
+ const body = (await response.json());
177
+ // Straight from base64 to bytes. Going via a string would corrupt every byte above 0x7F.
178
+ return toBytes(body.content ?? '');
179
+ }
180
+ async writeFile(path, contents, options) {
181
+ const branch = options.branch ?? this.branch;
182
+ const message = options.message;
183
+ // If the caller told us which revision it read, check it before writing. GitHub would also
184
+ // refuse a stale sha, but checking here produces a clearer error and avoids a wasted request.
185
+ if (options.sha !== undefined) {
186
+ const current = await this.currentSha(path, branch);
187
+ if (current !== options.sha) {
188
+ throw new ConflictError(`"${path}" changed since it was read.`);
189
+ }
190
+ }
191
+ const body = {
192
+ message,
193
+ branch,
194
+ content: typeof contents === 'string' ? encodeBase64(contents) : encodeBase64Bytes(contents),
195
+ ...(options.sha === undefined ? {} : { sha: options.sha }),
196
+ ...(this.author ? { author: this.author, committer: this.author } : {}),
197
+ };
198
+ const response = await this.call(`/repos/${this.owner}/${this.repo}/contents/${enc(path)}`, {
199
+ method: 'PUT',
200
+ body: JSON.stringify(body),
201
+ });
202
+ if (response.status === 409 || response.status === 422) {
203
+ throw new ConflictError(`"${path}" changed since it was read.`);
204
+ }
205
+ if (!response.ok)
206
+ throw new Error(await describe(response, `save ${path}`));
207
+ const result = (await response.json());
208
+ this.forget();
209
+ return {
210
+ ...(result.content?.sha ? { sha: result.content.sha } : {}),
211
+ ...(result.commit?.sha ? { commit: result.commit.sha } : {}),
212
+ };
213
+ }
214
+ async currentSha(path, branch) {
215
+ if (branch === this.branch) {
216
+ const tree = await this.loadTree();
217
+ return tree.get(path);
218
+ }
219
+ const response = await this.call(`/repos/${this.owner}/${this.repo}/contents/${enc(path)}?ref=${encodeURIComponent(branch)}`);
220
+ if (response.status === 404)
221
+ return undefined;
222
+ if (!response.ok)
223
+ return undefined;
224
+ return (await response.json()).sha;
225
+ }
226
+ async deleteFile(path, options) {
227
+ const sha = options.sha ?? (await this.currentSha(path, options.branch ?? this.branch));
228
+ if (!sha)
229
+ throw new NotFoundError(path);
230
+ const response = await this.call(`/repos/${this.owner}/${this.repo}/contents/${enc(path)}`, {
231
+ method: 'DELETE',
232
+ body: JSON.stringify({
233
+ message: options.message,
234
+ sha,
235
+ branch: options.branch ?? this.branch,
236
+ ...(this.author ? { author: this.author, committer: this.author } : {}),
237
+ }),
238
+ });
239
+ if (response.status === 404)
240
+ throw new NotFoundError(path);
241
+ if (response.status === 409 || response.status === 422) {
242
+ throw new ConflictError(`"${path}" changed since it was read.`);
243
+ }
244
+ if (!response.ok)
245
+ throw new Error(await describe(response, `delete ${path}`));
246
+ this.forget();
247
+ }
248
+ async listBranches() {
249
+ const response = await this.call(`/repos/${this.owner}/${this.repo}/branches?per_page=100`);
250
+ if (!response.ok)
251
+ throw new Error(await describe(response, 'list branches'));
252
+ return (await response.json()).map((branch) => branch.name);
253
+ }
254
+ async createBranch(name, from) {
255
+ const ref = await this.call(`/repos/${this.owner}/${this.repo}/git/ref/heads/${encodeURIComponent(from)}`);
256
+ if (!ref.ok)
257
+ throw new Error(await describe(ref, `find branch ${from}`));
258
+ const { object } = (await ref.json());
259
+ const created = await this.call(`/repos/${this.owner}/${this.repo}/git/refs`, {
260
+ method: 'POST',
261
+ body: JSON.stringify({ ref: `refs/heads/${name}`, sha: object.sha }),
262
+ });
263
+ // Already there is not a failure: creating a draft branch twice should be idempotent.
264
+ if (!created.ok && created.status !== 422) {
265
+ throw new Error(await describe(created, `create branch ${name}`));
266
+ }
267
+ }
268
+ /** Media lives in the repository for this backend, so it reuses the same writes. */
269
+ media() {
270
+ return {
271
+ upload: async (file, options) => {
272
+ const path = `${options.folder.replace(/\/+$/, '')}/${options.filename ?? file.name}`;
273
+ const result = await this.writeFile(path, file.data, { message: `Upload ${file.name}` });
274
+ return {
275
+ path,
276
+ url: `/${path}`,
277
+ size: file.data.byteLength,
278
+ name: file.name,
279
+ ...(result.sha ? { sha: result.sha } : {}),
280
+ };
281
+ },
282
+ list: async (folder) => (await this.listDir(folder))
283
+ .filter((entry) => entry.type === 'file')
284
+ .map((entry) => ({
285
+ path: `${folder}/${entry.name}`,
286
+ url: `/${folder}/${entry.name}`,
287
+ name: entry.name,
288
+ })),
289
+ remove: async (path) => {
290
+ await this.deleteFile(path, { message: `Delete ${path}` });
291
+ },
292
+ };
293
+ }
294
+ }
295
+ /** GitHub returns contents base64-encoded with newlines; atob needs them gone, and UTF-8 decoded. */
296
+ function decodeBase64(value) {
297
+ return new TextDecoder().decode(toBytes(value));
298
+ }
299
+ function toBytes(value) {
300
+ const binary = atob(value.replace(/\s/g, ''));
301
+ const bytes = new Uint8Array(binary.length);
302
+ for (let index = 0; index < binary.length; index += 1)
303
+ bytes[index] = binary.charCodeAt(index);
304
+ return bytes;
305
+ }
306
+ /** UTF-8 safe: btoa fails on any code point above U+00FF, which includes every CJK character. */
307
+ function encodeBase64(value) {
308
+ return encodeBase64Bytes(new TextEncoder().encode(value));
309
+ }
310
+ function encodeBase64Bytes(bytes) {
311
+ let binary = '';
312
+ const chunk = 0x8000;
313
+ for (let index = 0; index < bytes.length; index += chunk) {
314
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
315
+ }
316
+ return btoa(binary);
317
+ }
318
+ /** Turn an API error into something worth showing, including GitHub's own message. */
319
+ async function describe(response, action) {
320
+ if (response.status === 401)
321
+ return `GitHub rejected the token while trying to ${action}.`;
322
+ if (response.status === 403) {
323
+ const remaining = response.headers.get('x-ratelimit-remaining');
324
+ return remaining === '0'
325
+ ? `The GitHub rate limit was reached while trying to ${action}. Try again later.`
326
+ : `The token is not allowed to ${action}.`;
327
+ }
328
+ if (response.status === 404)
329
+ return `GitHub could not find what was needed to ${action}.`;
330
+ let detail = '';
331
+ try {
332
+ detail = (await response.json()).message ?? '';
333
+ }
334
+ catch {
335
+ /* A body is not guaranteed. */
336
+ }
337
+ return `Could not ${action}: GitHub returned ${response.status}${detail ? ` (${detail})` : ''}.`;
338
+ }
339
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../src/github/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Authentication for the GitHub backend.
3
+ *
4
+ * Two flows, and the CMS supports both because they suit different people:
5
+ *
6
+ * - A personal access token: paste it once, no server involved. Right for a local editor or a
7
+ * single author who does not want to run an OAuth relay.
8
+ * - OAuth: the CMS opens a popup to a relay that holds the client secret, and receives the token
9
+ * back over `postMessage`. Right for a deployed editor where authors should not handle tokens.
10
+ *
11
+ * The CMS never holds a client secret: that belongs on the relay, which is what makes the OAuth
12
+ * flow safe to ship in a static bundle.
13
+ */
14
+ export interface AuthSession {
15
+ token: string;
16
+ /** Where the token came from, so the UI can explain what will happen if it is dropped. */
17
+ origin: 'token' | 'oauth';
18
+ /** Login name, when the backend could be asked. */
19
+ login?: string;
20
+ }
21
+ export interface OAuthOptions {
22
+ /** Origin of the relay that performs the token exchange, e.g. `https://example.com`. */
23
+ authBase: string;
24
+ /** Endpoint path on that relay. Defaults to `auth`. */
25
+ authEndpoint?: string;
26
+ /** Where the editor lives, checked against the relay's reply. Defaults to `location.origin`. */
27
+ siteOrigin?: string;
28
+ }
29
+ /**
30
+ * Runs the popup handshake. Resolves with the token, or rejects with something worth showing the
31
+ * author — a closed window and a refused permission are different problems.
32
+ */
33
+ export declare function beginOAuth(options: OAuthOptions): Promise<AuthSession>;
34
+ /** Verify a token and return its owner, so a wrong token fails immediately and visibly. */
35
+ export declare function verifyToken(token: string, options?: {
36
+ apiRoot?: string;
37
+ fetch?: typeof globalThis.fetch;
38
+ }): Promise<AuthSession>;
39
+ /**
40
+ * Where a token is kept. `session` is the default because it dies with the tab: a token in
41
+ * `localStorage` outlives the session on a shared machine, and there is no way to encrypt it in a
42
+ * browser that would not be theatre.
43
+ */
44
+ export type TokenStorage = 'session' | 'local';
45
+ export declare const tokenStore: {
46
+ read(storage?: TokenStorage): string | undefined;
47
+ write(token: string, storage?: TokenStorage): void;
48
+ clear(): void;
49
+ };
50
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/github/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IAC1B,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,wFAAwF;IACxF,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAKD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAgDtE;AAED,2FAA2F;AAC3F,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAAO,GAClE,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC;AAG/C,eAAO,MAAM,UAAU;mBACP,YAAY,GAAe,MAAM,GAAG,SAAS;iBAO9C,MAAM,YAAW,YAAY,GAAe,IAAI;aAOpD,IAAI;CAQd,CAAC"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Authentication for the GitHub backend.
3
+ *
4
+ * Two flows, and the CMS supports both because they suit different people:
5
+ *
6
+ * - A personal access token: paste it once, no server involved. Right for a local editor or a
7
+ * single author who does not want to run an OAuth relay.
8
+ * - OAuth: the CMS opens a popup to a relay that holds the client secret, and receives the token
9
+ * back over `postMessage`. Right for a deployed editor where authors should not handle tokens.
10
+ *
11
+ * The CMS never holds a client secret: that belongs on the relay, which is what makes the OAuth
12
+ * flow safe to ship in a static bundle.
13
+ */
14
+ /** Decap's message shape, which relays in the wild already speak. */
15
+ const SUCCESS_PREFIX = 'authorization:github:success:';
16
+ /**
17
+ * Runs the popup handshake. Resolves with the token, or rejects with something worth showing the
18
+ * author — a closed window and a refused permission are different problems.
19
+ */
20
+ export function beginOAuth(options) {
21
+ const siteOrigin = options.siteOrigin ?? window.location.origin;
22
+ const url = new URL(`${options.authBase.replace(/\/$/, '')}/${options.authEndpoint ?? 'auth'}`);
23
+ url.searchParams.set('provider', 'github');
24
+ url.searchParams.set('site_id', siteOrigin);
25
+ return new Promise((resolve, reject) => {
26
+ const popup = window.open(url.href, 'v7-cms-oauth', 'width=720,height=820');
27
+ if (!popup) {
28
+ reject(new Error('The sign-in window was blocked. Allow popups and try again.'));
29
+ return;
30
+ }
31
+ const timer = {};
32
+ const cleanup = () => {
33
+ window.removeEventListener('message', onMessage);
34
+ if (timer.id !== undefined)
35
+ clearInterval(timer.id);
36
+ };
37
+ const onMessage = (event) => {
38
+ // Only the relay may answer, and only into this window.
39
+ if (event.origin !== new URL(options.authBase).origin)
40
+ return;
41
+ if (typeof event.data !== 'string')
42
+ return;
43
+ if (event.data === 'authorizing:github') {
44
+ popup.focus();
45
+ return;
46
+ }
47
+ if (!event.data.startsWith(SUCCESS_PREFIX))
48
+ return;
49
+ cleanup();
50
+ popup.close();
51
+ try {
52
+ const payload = JSON.parse(event.data.slice(SUCCESS_PREFIX.length));
53
+ if (!payload.token)
54
+ throw new Error('no token');
55
+ resolve({ token: payload.token, origin: 'oauth' });
56
+ }
57
+ catch {
58
+ reject(new Error('The sign-in reply could not be read.'));
59
+ }
60
+ };
61
+ window.addEventListener('message', onMessage);
62
+ // Detect the author closing the window rather than hanging forever.
63
+ timer.id = setInterval(() => {
64
+ if (popup.closed) {
65
+ cleanup();
66
+ reject(new Error('Sign-in was cancelled.'));
67
+ }
68
+ }, 500);
69
+ });
70
+ }
71
+ /** Verify a token and return its owner, so a wrong token fails immediately and visibly. */
72
+ export async function verifyToken(token, options = {}) {
73
+ const base = options.fetch ?? globalThis.fetch;
74
+ // A detached `fetch` loses its receiver, which the browser rejects as an illegal invocation.
75
+ const request = base === globalThis.fetch ? base.bind(globalThis) : base;
76
+ const apiRoot = options.apiRoot ?? 'https://api.github.com';
77
+ const response = await request(`${apiRoot}/user`, {
78
+ headers: {
79
+ Authorization: `Bearer ${token}`,
80
+ Accept: 'application/vnd.github+json',
81
+ },
82
+ });
83
+ if (!response.ok) {
84
+ throw new Error(response.status === 401
85
+ ? 'That token was rejected by GitHub.'
86
+ : `GitHub returned ${response.status} while checking the token.`);
87
+ }
88
+ const user = (await response.json());
89
+ return { token, origin: 'token', ...(user.login ? { login: user.login } : {}) };
90
+ }
91
+ const KEY = 'v7-cms-token';
92
+ export const tokenStore = {
93
+ read(storage = 'session') {
94
+ try {
95
+ return (storage === 'local' ? localStorage : sessionStorage).getItem(KEY) ?? undefined;
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ },
101
+ write(token, storage = 'session') {
102
+ try {
103
+ (storage === 'local' ? localStorage : sessionStorage).setItem(KEY, token);
104
+ }
105
+ catch {
106
+ /* Private mode: the session still works until the page is closed. */
107
+ }
108
+ },
109
+ clear() {
110
+ try {
111
+ sessionStorage.removeItem(KEY);
112
+ localStorage.removeItem(KEY);
113
+ }
114
+ catch {
115
+ /* Nothing to clear. */
116
+ }
117
+ },
118
+ };
119
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/github/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ export interface FakeGitHubOptions {
2
+ files?: Record<string, string>;
3
+ branch?: string;
4
+ /** Reject every request with this status, to exercise error handling. */
5
+ failWith?: {
6
+ status: number;
7
+ message?: string;
8
+ };
9
+ /** Fail only writes, leaving reads working. */
10
+ failWritesWith?: number;
11
+ }
12
+ export declare class FakeGitHub {
13
+ readonly calls: Array<{
14
+ method: string;
15
+ path: string;
16
+ body?: unknown;
17
+ }>;
18
+ /** Commits made, so tests can assert on messages and ordering. */
19
+ readonly commits: Array<{
20
+ message: string;
21
+ path: string;
22
+ branch: string;
23
+ }>;
24
+ private branches;
25
+ private options;
26
+ private defaultBranch;
27
+ constructor(options?: FakeGitHubOptions);
28
+ snapshot(branch?: string): Record<string, string>;
29
+ shaOf(path: string, branch?: string): string | undefined;
30
+ /** Simulates someone else committing, so conflict handling can be tested. */
31
+ externalEdit(path: string, text: string, branch?: string): void;
32
+ branchNames(): string[];
33
+ /** The fetch implementation to hand the adapter. */
34
+ fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
35
+ }
36
+ //# sourceMappingURL=fake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake.d.ts","sourceRoot":"","sources":["../../src/github/fake.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,qBAAa,UAAU;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAM;IAC7E,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAM;IAChF,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,aAAa,CAAS;gBAElB,OAAO,GAAE,iBAAsB;IAU3C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAKjD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIxD,6EAA6E;IAC7E,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAK/D,WAAW,IAAI,MAAM,EAAE;IAIvB,oDAAoD;IACpD,KAAK,GAAU,OAAO,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,OAAM,WAAgB,KAAG,OAAO,CAAC,QAAQ,CAAC,CAgItF;CACH"}