@openparachute/vault 0.6.3-rc.5 → 0.6.4-rc.1
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/package.json +1 -1
- package/src/admin-spa.test.ts +18 -5
- package/src/admin-spa.ts +24 -3
- package/src/mirror-credentials.test.ts +47 -0
- package/src/mirror-credentials.ts +33 -0
- package/src/mirror-manager.ts +38 -22
- package/src/mirror-routes.test.ts +78 -0
- package/src/mirror-routes.ts +13 -0
package/package.json
CHANGED
package/src/admin-spa.test.ts
CHANGED
|
@@ -38,9 +38,22 @@ describe("isAdminSpaPath", () => {
|
|
|
38
38
|
expect(isAdminSpaPath("/vault/work/admin/")).toBe(true);
|
|
39
39
|
expect(isAdminSpaPath("/vault/work/admin/tokens")).toBe(true);
|
|
40
40
|
expect(isAdminSpaPath("/vault/boulder/admin/assets/index.js")).toBe(true);
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
expect(isAdminSpaPath("/vault/my-
|
|
41
|
+
// Canonical-charset names (lowercase alphanumerics + hyphen / underscore)
|
|
42
|
+
// match; dots are NOT canonical (see the vault#253 reject test below).
|
|
43
|
+
expect(isAdminSpaPath("/vault/my-vault_2/admin")).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("vault#253: names outside the canonical charset do NOT match the mount", () => {
|
|
47
|
+
// The mount used to capture `[^/]+`, so a name hub can never manage
|
|
48
|
+
// (dots, @, unicode) still served the admin SPA. Aligning the mount to
|
|
49
|
+
// hub's VAULT_NAME_CHARSET_RE (`/^[a-z0-9_-]+$/`) makes a non-canonical
|
|
50
|
+
// name 404 — the same answer hub gives. No legitimately-created vault can
|
|
51
|
+
// carry these characters (cmdCreate / init / the env var all reject them
|
|
52
|
+
// via validateVaultName), so nothing reachable regresses.
|
|
53
|
+
expect(isAdminSpaPath("/vault/my.vault/admin")).toBe(false);
|
|
54
|
+
expect(isAdminSpaPath("/vault/vault@v2/admin")).toBe(false);
|
|
55
|
+
expect(isAdminSpaPath("/vault/Work/admin")).toBe(false); // uppercase isn't canonical
|
|
56
|
+
expect(isAdminSpaPath("/vault/🦑/admin")).toBe(false);
|
|
44
57
|
});
|
|
45
58
|
|
|
46
59
|
test("does not match adjacent paths under the same vault", () => {
|
|
@@ -111,8 +124,8 @@ describe("serveAdminSpa", () => {
|
|
|
111
124
|
expect(cssRes.headers.get("content-type")).toContain("text/css");
|
|
112
125
|
});
|
|
113
126
|
|
|
114
|
-
test("vault names
|
|
115
|
-
const res = await serveAdminSpa(fixtureDir, "/vault/my-
|
|
127
|
+
test("canonical-charset vault names strip cleanly", async () => {
|
|
128
|
+
const res = await serveAdminSpa(fixtureDir, "/vault/my-vault_2/admin/assets/index-abc.js");
|
|
116
129
|
expect(res.status).toBe(200);
|
|
117
130
|
expect(res.headers.get("content-type")).toContain("application/javascript");
|
|
118
131
|
});
|
package/src/admin-spa.ts
CHANGED
|
@@ -21,12 +21,33 @@ import { existsSync } from "node:fs";
|
|
|
21
21
|
import { dirname, join, resolve } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The canonical vault-name charset, kept byte-identical with hub's
|
|
26
|
+
* `VAULT_NAME_CHARSET_RE` (`parachute-hub/src/vault-name.ts`) and vault's own
|
|
27
|
+
* `validateVaultName` (`src/vault-name.ts`): lowercase alphanumerics plus
|
|
28
|
+
* hyphen / underscore. Embedded here (not imported) because this regex is
|
|
29
|
+
* spliced into the mount-matching patterns below, and a charset class is the
|
|
30
|
+
* only piece they share.
|
|
31
|
+
*
|
|
32
|
+
* vault#253: the mount regexes used to capture `[^/]+` (anything-but-slash),
|
|
33
|
+
* so a name like `my.vault` / `vault@v2` / `🦑` matched the admin mount and
|
|
34
|
+
* served the SPA — even though hub rejects those names at every name-minting
|
|
35
|
+
* edge and can never render a "Manage Vault" link for them. A vault that can't
|
|
36
|
+
* be created (cmdCreate / init / the env var all run `validateVaultName`, which
|
|
37
|
+
* rejects out-of-charset names) shouldn't have a reachable admin mount either.
|
|
38
|
+
* Pinning the mount to THIS charset closes the boundary drift: a dotted name
|
|
39
|
+
* no longer matches → 404, the same answer hub gives.
|
|
40
|
+
*/
|
|
41
|
+
const VAULT_NAME_CHARSET = "a-z0-9_-";
|
|
42
|
+
|
|
24
43
|
/**
|
|
25
44
|
* Regex anchoring the per-vault SPA mount. Matches `/vault/<name>/admin`
|
|
26
|
-
* exactly and any subpath under it
|
|
27
|
-
*
|
|
45
|
+
* exactly and any subpath under it, where `<name>` is a canonical vault name
|
|
46
|
+
* (lowercase alphanumerics + hyphen / underscore — see `VAULT_NAME_CHARSET`).
|
|
47
|
+
* The `<name>` capture is reused by the prefix-strip below — keep the two in
|
|
48
|
+
* sync if this regex moves.
|
|
28
49
|
*/
|
|
29
|
-
const ADMIN_SPA_MOUNT_RE =
|
|
50
|
+
const ADMIN_SPA_MOUNT_RE = new RegExp(`^/vault/([${VAULT_NAME_CHARSET}]+)/admin(?=/|$)`);
|
|
30
51
|
|
|
31
52
|
/**
|
|
32
53
|
* Regex anchoring the DAEMON-LEVEL multi-vault SPA mount at `/vault/admin`
|
|
@@ -104,6 +104,53 @@ describe("serialize + parse round-trip", () => {
|
|
|
104
104
|
expect(out).toEqual(creds);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
test("github_oauth with persisted owner/name round-trips through the bytes (vault#401)", () => {
|
|
108
|
+
// owner/name are persisted at repo-select time so the selection survives a
|
|
109
|
+
// cleared git origin + restart. They must serialize and parse back
|
|
110
|
+
// byte-faithfully.
|
|
111
|
+
const creds: MirrorCredentials = {
|
|
112
|
+
active_method: "github_oauth",
|
|
113
|
+
github_oauth: {
|
|
114
|
+
access_token: "gho_abc123def456ghi789",
|
|
115
|
+
scope: "repo",
|
|
116
|
+
authorized_at: "2026-05-28T03:14:15.000Z",
|
|
117
|
+
user_login: "aaron",
|
|
118
|
+
user_id: 12345,
|
|
119
|
+
owner: "aaron",
|
|
120
|
+
name: "backup-repo",
|
|
121
|
+
},
|
|
122
|
+
pat: null,
|
|
123
|
+
};
|
|
124
|
+
const out = parseCredentials(serializeCredentials(creds));
|
|
125
|
+
expect(out).toEqual(creds);
|
|
126
|
+
expect(out.github_oauth?.owner).toBe("aaron");
|
|
127
|
+
expect(out.github_oauth?.name).toBe("backup-repo");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("github_oauth WITHOUT owner/name stays byte-stable (back-compat)", () => {
|
|
131
|
+
// Credentials written before a repo is picked (or before vault#401) carry
|
|
132
|
+
// no owner/name. Serialize must NOT emit empty owner/name lines, and the
|
|
133
|
+
// round-trip must leave the fields absent (not "").
|
|
134
|
+
const creds: MirrorCredentials = {
|
|
135
|
+
active_method: "github_oauth",
|
|
136
|
+
github_oauth: {
|
|
137
|
+
access_token: "gho_abc123def456ghi789",
|
|
138
|
+
scope: "repo",
|
|
139
|
+
authorized_at: "2026-05-28T03:14:15.000Z",
|
|
140
|
+
user_login: "aaron",
|
|
141
|
+
user_id: 12345,
|
|
142
|
+
},
|
|
143
|
+
pat: null,
|
|
144
|
+
};
|
|
145
|
+
const serialized = serializeCredentials(creds);
|
|
146
|
+
expect(serialized).not.toContain("owner:");
|
|
147
|
+
expect(serialized).not.toContain("name:");
|
|
148
|
+
const out = parseCredentials(serialized);
|
|
149
|
+
expect(out).toEqual(creds);
|
|
150
|
+
expect(out.github_oauth?.owner).toBeUndefined();
|
|
151
|
+
expect(out.github_oauth?.name).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
107
154
|
test("github_oauth with empty scope round-trips (GitHub App tokens)", () => {
|
|
108
155
|
// Tokens from the shared Parachute GitHub App carry scope: "" (GitHub
|
|
109
156
|
// Apps use fine-grained permissions, not scopes). The empty string must
|
|
@@ -86,6 +86,18 @@ export interface GitHubOAuthCredential {
|
|
|
86
86
|
user_login: string;
|
|
87
87
|
/** GitHub numeric user id — stable across login renames. */
|
|
88
88
|
user_id: number;
|
|
89
|
+
/**
|
|
90
|
+
* The selected repo's owner — persisted at repo-select time (vault#401).
|
|
91
|
+
* Optional: absent before the operator picks a repo, and absent in
|
|
92
|
+
* credentials files written before this field existed (back-compat). When
|
|
93
|
+
* present, `applyCredentialsToRemote` reconstructs the authed remote URL
|
|
94
|
+
* from `owner`/`name` directly rather than regex-parsing the current git
|
|
95
|
+
* origin — so the selection survives the origin being cleared (e.g.
|
|
96
|
+
* `DELETE /auth` then re-OAuth) and a server restart.
|
|
97
|
+
*/
|
|
98
|
+
owner?: string;
|
|
99
|
+
/** The selected repo's name — persisted alongside `owner` (vault#401). */
|
|
100
|
+
name?: string;
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
/**
|
|
@@ -138,6 +150,10 @@ export interface MirrorCredentialsPublic {
|
|
|
138
150
|
scope: string;
|
|
139
151
|
authorized_at: string;
|
|
140
152
|
token_preview: string;
|
|
153
|
+
/** Selected repo owner, when a repo has been picked (vault#401). */
|
|
154
|
+
owner?: string;
|
|
155
|
+
/** Selected repo name, when a repo has been picked (vault#401). */
|
|
156
|
+
name?: string;
|
|
141
157
|
} | null;
|
|
142
158
|
pat: {
|
|
143
159
|
label: string;
|
|
@@ -233,6 +249,15 @@ export function serializeCredentials(creds: MirrorCredentials): string {
|
|
|
233
249
|
lines.push(` authorized_at: ${quoteIfNeeded(creds.github_oauth.authorized_at)}`);
|
|
234
250
|
lines.push(` user_login: ${quoteIfNeeded(creds.github_oauth.user_login)}`);
|
|
235
251
|
lines.push(` user_id: ${creds.github_oauth.user_id}`);
|
|
252
|
+
// owner/name persisted at repo-select time (vault#401). Emitted only when
|
|
253
|
+
// present so credentials written before a repo is picked (or before this
|
|
254
|
+
// field existed) stay byte-stable.
|
|
255
|
+
if (creds.github_oauth.owner !== undefined) {
|
|
256
|
+
lines.push(` owner: ${quoteIfNeeded(creds.github_oauth.owner)}`);
|
|
257
|
+
}
|
|
258
|
+
if (creds.github_oauth.name !== undefined) {
|
|
259
|
+
lines.push(` name: ${quoteIfNeeded(creds.github_oauth.name)}`);
|
|
260
|
+
}
|
|
236
261
|
} else {
|
|
237
262
|
lines.push("github_oauth: null");
|
|
238
263
|
}
|
|
@@ -356,6 +381,8 @@ export function parseCredentials(yaml: string): MirrorCredentials {
|
|
|
356
381
|
else if (key === "scope") oauth.scope = parseScalar(rawVal!);
|
|
357
382
|
else if (key === "authorized_at") oauth.authorized_at = parseScalar(rawVal!);
|
|
358
383
|
else if (key === "user_login") oauth.user_login = parseScalar(rawVal!);
|
|
384
|
+
else if (key === "owner") oauth.owner = parseScalar(rawVal!);
|
|
385
|
+
else if (key === "name") oauth.name = parseScalar(rawVal!);
|
|
359
386
|
else if (key === "user_id") {
|
|
360
387
|
const n = Number(parseScalar(rawVal!));
|
|
361
388
|
if (Number.isFinite(n)) oauth.user_id = n;
|
|
@@ -571,6 +598,12 @@ export function sanitizeCredentials(
|
|
|
571
598
|
scope: creds.github_oauth.scope,
|
|
572
599
|
authorized_at: creds.github_oauth.authorized_at,
|
|
573
600
|
token_preview: previewToken(creds.github_oauth.access_token),
|
|
601
|
+
...(creds.github_oauth.owner !== undefined
|
|
602
|
+
? { owner: creds.github_oauth.owner }
|
|
603
|
+
: {}),
|
|
604
|
+
...(creds.github_oauth.name !== undefined
|
|
605
|
+
? { name: creds.github_oauth.name }
|
|
606
|
+
: {}),
|
|
574
607
|
}
|
|
575
608
|
: null,
|
|
576
609
|
pat: creds.pat
|
package/src/mirror-manager.ts
CHANGED
|
@@ -1073,33 +1073,49 @@ export class MirrorManager {
|
|
|
1073
1073
|
return;
|
|
1074
1074
|
}
|
|
1075
1075
|
// github_oauth: the active token is set, but a repo may not yet be
|
|
1076
|
-
// picked. The select-repo route handler writes the URL
|
|
1077
|
-
//
|
|
1078
|
-
// owner/repo. Best-effort: if `origin` already points at a github.com
|
|
1079
|
-
// URL, rewrite it with the stored token in case the token rotated.
|
|
1076
|
+
// picked. The select-repo route handler writes the URL onto origin AND
|
|
1077
|
+
// (vault#401) persists owner/name into the credentials struct.
|
|
1080
1078
|
if (creds.active_method === "github_oauth" && creds.github_oauth) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1079
|
+
// vault#401: prefer the persisted owner/name. Reconstructing the
|
|
1080
|
+
// remote from credentials alone means the selection survives the
|
|
1081
|
+
// origin being cleared (DELETE /auth then re-OAuth) across a restart —
|
|
1082
|
+
// the bug class where regex-parsing the origin silently lost the repo.
|
|
1083
|
+
const persistedOwner = creds.github_oauth.owner;
|
|
1084
|
+
const persistedName = creds.github_oauth.name;
|
|
1085
|
+
let owner: string | undefined;
|
|
1086
|
+
let repo: string | undefined;
|
|
1087
|
+
if (persistedOwner && persistedName) {
|
|
1088
|
+
owner = persistedOwner;
|
|
1089
|
+
repo = persistedName;
|
|
1090
|
+
} else {
|
|
1091
|
+
// Back-compat fallback: credentials written before #401 don't carry
|
|
1092
|
+
// owner/name. Parse them out of the current github.com origin (the
|
|
1093
|
+
// pre-#401 behavior) when origin is still present.
|
|
1094
|
+
const current = await readCurrentOrigin(repoDir);
|
|
1095
|
+
if (current && current.includes("github.com")) {
|
|
1096
|
+
const match = current.match(
|
|
1097
|
+
/github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?$/,
|
|
1094
1098
|
);
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
`[mirror] could not refresh github_oauth remote URL (non-fatal): ${result.error}`,
|
|
1099
|
-
);
|
|
1099
|
+
if (match) {
|
|
1100
|
+
owner = match[1];
|
|
1101
|
+
repo = match[2];
|
|
1100
1102
|
}
|
|
1101
1103
|
}
|
|
1102
1104
|
}
|
|
1105
|
+
if (owner && repo) {
|
|
1106
|
+
const { githubAuthedRemoteUrl } = await import("./mirror-credentials.ts");
|
|
1107
|
+
const authed = githubAuthedRemoteUrl(
|
|
1108
|
+
creds.github_oauth.access_token,
|
|
1109
|
+
owner,
|
|
1110
|
+
repo,
|
|
1111
|
+
);
|
|
1112
|
+
const result = await applyToGitRemote(repoDir, authed);
|
|
1113
|
+
if (!result.ok) {
|
|
1114
|
+
console.warn(
|
|
1115
|
+
`[mirror] could not refresh github_oauth remote URL (non-fatal): ${result.error}`,
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1103
1119
|
}
|
|
1104
1120
|
} catch (err) {
|
|
1105
1121
|
console.warn(
|
|
@@ -912,6 +912,84 @@ describe("auth credential routes — credential-save side-effects (Cuts 3 + 6)",
|
|
|
912
912
|
await manager.stop();
|
|
913
913
|
}, 30_000);
|
|
914
914
|
|
|
915
|
+
test("vault#401: owner/name survive a cleared origin + restart (persisted in creds, not just the git remote)", async () => {
|
|
916
|
+
// The bug: pre-#401, select-repo wrote owner/name ONLY into the git
|
|
917
|
+
// origin URL. On restart, applyCredentialsToRemote regex-parsed them back
|
|
918
|
+
// out of the origin — so clearing the origin (DELETE /auth → re-OAuth
|
|
919
|
+
// without re-selecting) silently lost the repo even with a valid token.
|
|
920
|
+
// Now select-repo persists owner/name into the credentials struct, and
|
|
921
|
+
// applyCredentialsToRemote prefers the persisted values. With the origin
|
|
922
|
+
// cleared, a restart still reconstructs the github.com remote.
|
|
923
|
+
home = tmp("mirror-selectrepo-persist-");
|
|
924
|
+
const { manager, deps } = makeManager(home);
|
|
925
|
+
const cfg = {
|
|
926
|
+
...defaultMirrorConfig(),
|
|
927
|
+
enabled: true,
|
|
928
|
+
location: "internal" as const,
|
|
929
|
+
sync_mode: "manual" as const,
|
|
930
|
+
auto_commit: false,
|
|
931
|
+
auto_push: false,
|
|
932
|
+
};
|
|
933
|
+
// The manager reads config via deps.readMirrorConfig (in-memory); the
|
|
934
|
+
// route's history-on-link keys off the real per-vault file's existence.
|
|
935
|
+
// Seed both, same as the autopush test above.
|
|
936
|
+
deps.writeMirrorConfig(cfg);
|
|
937
|
+
writeMirrorConfigForVault("default", cfg);
|
|
938
|
+
await manager.start();
|
|
939
|
+
const mirrorPath = manager.getStatus().mirror_path;
|
|
940
|
+
expect(mirrorPath).toBeTruthy();
|
|
941
|
+
|
|
942
|
+
writeCredentials("default", {
|
|
943
|
+
active_method: "github_oauth",
|
|
944
|
+
github_oauth: {
|
|
945
|
+
access_token: "gho_persisttoken12345",
|
|
946
|
+
scope: "repo",
|
|
947
|
+
authorized_at: "2026-05-28T03:14:15.000Z",
|
|
948
|
+
user_login: "aaron",
|
|
949
|
+
user_id: 1,
|
|
950
|
+
},
|
|
951
|
+
pat: null,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const res = await handleAuthGithubSelectRepo(
|
|
955
|
+
new Request("http://x/select", {
|
|
956
|
+
method: "POST",
|
|
957
|
+
body: JSON.stringify({ owner: "aaron", name: "backup-repo" }),
|
|
958
|
+
}),
|
|
959
|
+
manager,
|
|
960
|
+
);
|
|
961
|
+
expect(res.status).toBe(200);
|
|
962
|
+
|
|
963
|
+
// The fix: owner/name persisted into the credentials struct (not just origin).
|
|
964
|
+
const persisted = readCredentials("default");
|
|
965
|
+
expect(persisted?.github_oauth?.owner).toBe("aaron");
|
|
966
|
+
expect(persisted?.github_oauth?.name).toBe("backup-repo");
|
|
967
|
+
|
|
968
|
+
// Simulate the DELETE /auth + re-OAuth-without-reselect path: the OAuth
|
|
969
|
+
// token survives, but the git origin gets cleared.
|
|
970
|
+
Bun.spawnSync(["git", "remote", "remove", "origin"], { cwd: mirrorPath! });
|
|
971
|
+
const afterClear = Bun.spawnSync(["git", "remote", "get-url", "origin"], {
|
|
972
|
+
cwd: mirrorPath!,
|
|
973
|
+
});
|
|
974
|
+
expect(afterClear.exitCode).not.toBe(0); // origin is gone
|
|
975
|
+
|
|
976
|
+
// Restart — applyCredentialsToRemote runs again. Pre-#401 it would find no
|
|
977
|
+
// origin to parse and leave the remote unset; post-#401 it rebuilds from
|
|
978
|
+
// the persisted owner/name.
|
|
979
|
+
await manager.stop();
|
|
980
|
+
await manager.start();
|
|
981
|
+
|
|
982
|
+
const reapplied = Bun.spawnSync(["git", "remote", "get-url", "origin"], {
|
|
983
|
+
cwd: mirrorPath!,
|
|
984
|
+
});
|
|
985
|
+
expect(reapplied.exitCode).toBe(0);
|
|
986
|
+
const reappliedUrl = reapplied.stdout.toString().trim();
|
|
987
|
+
// The remote was reconstructed from persisted owner/name (token embedded,
|
|
988
|
+
// not asserted here). Without persistence this would be empty.
|
|
989
|
+
expect(reappliedUrl).toContain("github.com/aaron/backup-repo");
|
|
990
|
+
await manager.stop();
|
|
991
|
+
}, 30_000);
|
|
992
|
+
|
|
915
993
|
test("select-repo is a no-op for auto_push when mirror is disabled", async () => {
|
|
916
994
|
// Operator wiring credentials before flipping the mirror on — don't
|
|
917
995
|
// mutate auto_push behind their back.
|
package/src/mirror-routes.ts
CHANGED
|
@@ -1200,6 +1200,19 @@ export async function handleAuthGithubSelectRepo(
|
|
|
1200
1200
|
}
|
|
1201
1201
|
}
|
|
1202
1202
|
|
|
1203
|
+
// vault#401: persist the selected owner/name INTO the credentials struct,
|
|
1204
|
+
// not only into the git origin URL. Pre-#401 the selection lived solely in
|
|
1205
|
+
// the origin URL; `applyCredentialsToRemote` regex-parsed it back out on
|
|
1206
|
+
// restart, so clearing the origin (e.g. `DELETE /auth` then re-OAuth without
|
|
1207
|
+
// re-selecting) silently lost the repo even with a still-valid token. Saving
|
|
1208
|
+
// it here lets the restart path reconstruct the remote from credentials
|
|
1209
|
+
// alone. Write before the remote-apply so the persisted selection survives
|
|
1210
|
+
// even if the apply later fails.
|
|
1211
|
+
writeCredentials(manager.getVaultName(), {
|
|
1212
|
+
...creds,
|
|
1213
|
+
github_oauth: { ...creds.github_oauth, owner, name },
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1203
1216
|
// Reach into mirror-credentials.ts for the authed URL builder.
|
|
1204
1217
|
const { githubAuthedRemoteUrl } = await import("./mirror-credentials.ts");
|
|
1205
1218
|
const authedUrl = githubAuthedRemoteUrl(
|