@bongos/core 1.21.94 → 1.21.96

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.
@@ -37,8 +37,17 @@ const SETUP_EVENTS = Object.freeze({
37
37
  installReported: 'github-app-install-reported',
38
38
  installed: 'github-app-installed',
39
39
  deviceFlow: 'github-app-device-flow',
40
+ // the organisation the founder chose to make the app on (task 1004651); its detail is
41
+ // the org's login. Written by the mint route; absent = the founder's personal account.
42
+ appOrg: 'github-app-org',
40
43
  });
41
44
 
45
+ // A GitHub login (1-39 chars, letters/digits and single inner hyphens). The org name the
46
+ // mint route takes from the page passes this before it reaches a GitHub URL, and the
47
+ // recorded one passes it again before it reaches a link. PURE.
48
+ const GITHUB_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
49
+ function validGithubLogin(s) { return typeof s === 'string' && GITHUB_LOGIN_RE.test(s); }
50
+
42
51
  // How long after an attempt settles the runner keeps checking for the install and the
43
52
  // device-flow tick. Past it the checklist stops promising a self-tick for device flow
44
53
  // (the step says "turn it on later"); a fresh install report re-opens the window.
@@ -65,16 +74,23 @@ function targetRepo(inst) {
65
74
  // The verified facts for one instance since `since` (the attempt's created_at): when each
66
75
  // setup event was first seen, or null. One indexed read (idx_provisioning_events_instance).
67
76
  async function githubSetupFacts(db, instanceId, since) {
77
+ // `detail` is the newest row's, for the one event that carries a value (appOrg)
68
78
  const { rows } = await db.query(
69
- `SELECT event, min(created_at) AS at FROM provisioning_events
79
+ `SELECT event, min(created_at) AS at, (array_agg(detail ORDER BY created_at DESC))[1] AS detail
80
+ FROM provisioning_events
70
81
  WHERE instance_id = $1 AND event = ANY($2) AND created_at >= $3
71
82
  GROUP BY event`,
72
83
  [instanceId, Object.values(SETUP_EVENTS), since || new Date(0).toISOString()]);
84
+ const find = (name) => rows.find((x) => x.event === name);
73
85
  const at = (name) => {
74
- const r = rows.find((x) => x.event === name);
86
+ const r = find(name);
75
87
  return r && r.at ? new Date(r.at).toISOString() : null;
76
88
  };
77
- return { installReportedAt: at(SETUP_EVENTS.installReported), installedAt: at(SETUP_EVENTS.installed), deviceFlowAt: at(SETUP_EVENTS.deviceFlow) };
89
+ const org = find(SETUP_EVENTS.appOrg);
90
+ return {
91
+ installReportedAt: at(SETUP_EVENTS.installReported), installedAt: at(SETUP_EVENTS.installed), deviceFlowAt: at(SETUP_EVENTS.deviceFlow),
92
+ appOrg: org && validGithubLogin(org.detail) ? org.detail : null,
93
+ };
78
94
  }
79
95
 
80
96
  // Each checklist step's state, from the latest attempt's EFFECTIVE status (manifest-truth's
@@ -113,7 +129,7 @@ function stillVerifying(row, facts = {}, now = Date.now()) {
113
129
  // lands on the setup page, which sends the founder back to their projects.
114
130
  // A failed facts read is "nothing verified yet" — the honest 'now' states, never a 500.
115
131
  async function githubSetupRead(db, inst, row, status, { now = Date.now(), withState = true } = {}) {
116
- if (!row) return { checklist: githubChecklist(null, null), install_url: null, repo: targetRepo(inst), verifying: false };
132
+ if (!row) return { checklist: githubChecklist(null, null), install_url: null, repo: targetRepo(inst), verifying: false, app_org: null };
117
133
  let facts = {};
118
134
  // through module.exports so a route test can stub the one DB read on the cached module
119
135
  try { facts = await module.exports.githubSetupFacts(db, row.instance_id, row.created_at); } catch (_) { facts = {}; }
@@ -123,7 +139,13 @@ async function githubSetupRead(db, inst, row, status, { now = Date.now(), withSt
123
139
  install_url: checklist.install === 'done' ? null : installUrlFor({ appSlug: row.app_slug, state: withState ? row.state : null }),
124
140
  repo: targetRepo(inst),
125
141
  verifying: stillVerifying(row, facts, now),
142
+ // the organisation this attempt makes the app on, or null for the founder's account
143
+ // (task 1004651): the page links the app's settings under the right owner
144
+ app_org: validGithubLogin(facts.appOrg) ? facts.appOrg : null,
126
145
  };
127
146
  }
128
147
 
129
- module.exports = { SETUP_EVENTS, VERIFY_WINDOW_MS, installUrlFor, targetRepo, githubSetupFacts, githubChecklist, stillVerifying, githubSetupRead };
148
+ module.exports = {
149
+ SETUP_EVENTS, VERIFY_WINDOW_MS, installUrlFor, targetRepo, githubSetupFacts, githubChecklist, stillVerifying, githubSetupRead,
150
+ validGithubLogin,
151
+ };
@@ -27,6 +27,16 @@ function esc(s) {
27
27
  ));
28
28
  }
29
29
 
30
+ // The way back to the project (task 1004650): the hub's return route, keyed on the attempt's
31
+ // one-time state. The SERVER resolves that state to the project it was minted for and lands
32
+ // its signed-in owner on that project's part 03 (routes/github-setup.js), so the hand-back no
33
+ // longer depends on what the starting tab kept in sessionStorage — approving on a phone or in
34
+ // another window lands on the right project. No state names no project: the projects list.
35
+ const RETURN_PATH = '/api/bongos/provisioning/github-app/return';
36
+ function returnHref(state) {
37
+ return state ? `${RETURN_PATH}?state=${encodeURIComponent(state)}` : '/projects?view=mine';
38
+ }
39
+
30
40
  // Accent pair resolved from the instance's own branding — the pack is the palette
31
41
  // authority (task 1002810); the literal fallbacks are the neutral pack's values, so
32
42
  // a branding validation error still paints the current family, never a stale one.
@@ -65,15 +75,15 @@ code{font-family:"Fira Code",ui-monospace,SFMono-Regular,monospace;background:rg
65
75
  // page hands them straight back: as the wizard's pop-up it closes itself so the checklist
66
76
  // tab is in front again (that tab is already polling the same status and shows the wait);
67
77
  // if the browser refuses the close, or this was a same-tab return (pop-up blocked), this
68
- // tab becomes the checklist. The one-time state rides the URL FRAGMENT — never sent to a
69
- // server — so a checklist opened here can keep watching the same attempt.
78
+ // tab becomes the checklist. The way back is the return route (returnHref above): the
79
+ // server names the project for the state, so the checklist opened here is the right one
80
+ // whatever this browser remembers (task 1004650).
70
81
  // Deliberately contains NO backticks and NO ${...} so it survives the outer template
71
82
  // literal verbatim; the only server data it reads is the JSON config block above it.
72
83
  const HANDBACK_SCRIPT = `
73
84
  (function(){
74
85
  var cfg=window.__CB_SETUP__||{};
75
- var back=cfg.back||'/projects?view=new';
76
- if(cfg.state)back+='#ghstate='+encodeURIComponent(cfg.state);
86
+ var back=cfg.back||'/projects?view=mine';
77
87
  var link=document.getElementById('back');if(link)link.href=back;
78
88
  var popup=false;
79
89
  try{popup=!!window.opener&&!window.opener.closed;}catch(e){}
@@ -94,8 +104,7 @@ const HANDBACK_SCRIPT = `
94
104
  const INSTALL_SCRIPT = `
95
105
  (function(){
96
106
  var cfg=window.__CB_SETUP__||{};
97
- var back=cfg.back||'/projects?view=new';
98
- if(cfg.state)back+='#ghstate='+encodeURIComponent(cfg.state);
107
+ var back=cfg.back||'/projects?view=mine';
99
108
  var link=document.getElementById('back');if(link)link.href=back;
100
109
  var note=document.getElementById('note');
101
110
  var say=function(t){if(note)note.textContent=t;};
@@ -142,7 +151,8 @@ const INSTALL_SCRIPT = `
142
151
  // + `body` (server-authored, trusted HTML).
143
152
  function callbackPage({ ok, title, body, state, domain, next }) {
144
153
  const handback = !!(ok && state && domain);
145
- const cfgJson = JSON.stringify({ state: state || '', domain: domain || '', back: '/projects?view=new' }).replace(/</g, '\\u003c');
154
+ const back = returnHref(state);
155
+ const cfgJson = JSON.stringify({ state: state || '', domain: domain || '', back }).replace(/</g, '\\u003c');
146
156
  const head = `<!doctype html><html lang="en"><head><meta charset="utf-8">
147
157
  <meta name="viewport" content="width=device-width, initial-scale=1">
148
158
  <meta name="color-scheme" content="light dark">
@@ -157,7 +167,7 @@ function callbackPage({ ok, title, body, state, domain, next }) {
157
167
  <h1>${esc(title)}</h1>
158
168
  <p class="sub">Next, install it on your repo. Taking you there now.</p>
159
169
  <p class="muted" id="note" role="status" aria-live="polite">Waiting for GitHub to hand over the app…</p>
160
- <div class="cta"><a class="btn" id="back" href="/projects?view=new">Back to my project</a></div>
170
+ <div class="cta"><a class="btn" id="back" href="${esc(back)}">Back to my project</a></div>
161
171
  </main>
162
172
  <script>window.__CB_SETUP__=${cfgJson};</script>
163
173
  <script>${INSTALL_SCRIPT}</script>
@@ -169,7 +179,7 @@ function callbackPage({ ok, title, body, state, domain, next }) {
169
179
  <div class="badge-warn">✓</div>
170
180
  <h1>${esc(title)}</h1>
171
181
  <p class="sub">Taking you back to your setup checklist — it finishes setting up <code>${esc(domain)}</code> there.</p>
172
- <div class="cta"><a class="btn" id="back" href="/projects?view=new">Back to my checklist →</a></div>
182
+ <div class="cta"><a class="btn" id="back" href="${esc(back)}">Back to my checklist →</a></div>
173
183
  </main>
174
184
  <script>window.__CB_SETUP__=${cfgJson};</script>
175
185
  <script>${HANDBACK_SCRIPT}</script>
@@ -184,4 +194,4 @@ function callbackPage({ ok, title, body, state, domain, next }) {
184
194
  </body></html>`;
185
195
  }
186
196
 
187
- module.exports = { esc, callbackPage };
197
+ module.exports = { esc, callbackPage, returnHref, RETURN_PATH };
@@ -9,6 +9,12 @@
9
9
  // single-use `state` minted for the signed-in owner is the only key, and neither route
10
10
  // reveals a secret or changes authority. The checklist they report is derived in
11
11
  // ../github-setup.js from what the control-plane runner has VERIFIED.
12
+ //
13
+ // Two signed-in routes join them (task 1004650 — the hand-back from any tab or device):
14
+ // the RETURN route the callback and setup pages send the founder to, which lands the state's
15
+ // own project for its owner and nobody else, and the RESUME read the project page asks when
16
+ // its tab remembers nothing — so a phone, a second window or a fresh tab picks up part 03 of
17
+ // the right project from what the server knows, never from sessionStorage.
12
18
  'use strict';
13
19
 
14
20
  const express = require('express');
@@ -17,10 +23,38 @@ const { pool } = api;
17
23
  const provisioning = require('../provisioning');
18
24
  const githubSetup = require('../github-setup');
19
25
  const { makeAskRateLimiter } = require('../rate-limit');
20
- const { callbackPage } = require('./callback-page');
26
+ const { callbackPage, returnHref } = require('./callback-page');
21
27
 
22
28
  const log = api.logger('provisioning');
23
29
 
30
+ // The state tokens this leg mints are hex (provisioning.js); anything else is not one of ours.
31
+ const STATE_RE = /^[0-9a-f]{16,128}$/i;
32
+
33
+ // An attempt or a project older than this is not "in progress" for the resume read: a
34
+ // founder who walked away a week ago and opens the create page gets a fresh page.
35
+ const RESUME_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
36
+
37
+ // Is this instance one the hand-back may land on for this builder? Strictly the OWNER —
38
+ // no Archon override: the return route names a project to whoever holds the state, and a
39
+ // project page is where its owner, and only its owner, connects GitHub. A gone project is
40
+ // no landing either. PURE.
41
+ function ownsLanding(inst, builderId) {
42
+ return !!(inst && builderId != null && String(inst.owner_builder_id) === String(builderId)
43
+ && !inst.torn_down_at && inst.status !== 'torn_down');
44
+ }
45
+
46
+ // The project page's resume record: just what part 03 needs to name and watch the project
47
+ // (the page reads the rest from the owner's instance read). Never the state token. PURE.
48
+ function resumeRecord(inst) {
49
+ return { id: inst.id, slug: inst.slug, domain: inst.domain || null, onboard_mode: inst.onboard_mode || 'greenfield', repo: githubSetup.targetRepo(inst) };
50
+ }
51
+
52
+ // The project page to land on: part 03 of that project, by id. The state rides the
53
+ // FRAGMENT (never sent to a server) so the page can keep watching that attempt. PURE.
54
+ function landingHref(instanceId, state) {
55
+ return `/projects?view=new&id=${encodeURIComponent(String(instanceId))}${state ? `#ghstate=${encodeURIComponent(state)}` : ''}`;
56
+ }
57
+
24
58
  // GitHub's installation id is digits; anything else is dropped before it reaches a log line.
25
59
  function installationIdOf(q) {
26
60
  const s = String((q && q.installation_id) || '');
@@ -32,6 +66,7 @@ module.exports = function buildGithubSetupRouter() {
32
66
  // the page polls every few seconds, the callback page every two — per-IP, in-module
33
67
  const statusRateLimit = makeAskRateLimiter({ max: 120 });
34
68
  const setupRateLimit = makeAskRateLimiter({ max: 30 });
69
+ const returnRateLimit = makeAskRateLimiter({ max: 30 });
35
70
 
36
71
  // GET /provisioning/github-app/status?state=<> — the PUBLIC progress read (task 1004548;
37
72
  // the checklist since task 1004649). Reveals the manifest lifecycle status, the
@@ -65,6 +100,7 @@ module.exports = function buildGithubSetupRouter() {
65
100
  install_url: setup.install_url,
66
101
  repo: setup.repo,
67
102
  verifying: setup.verifying,
103
+ app_org: setup.app_org,
68
104
  });
69
105
  } catch (err) {
70
106
  log.error('[provisioning] GET /provisioning/github-app/status', err);
@@ -85,7 +121,7 @@ module.exports = function buildGithubSetupRouter() {
85
121
  const sendPage = (status, page) => res.status(status).type('html').send(page);
86
122
  try {
87
123
  const state = String(req.query.state || '');
88
- const row = /^[0-9a-f]{16,128}$/i.test(state) ? await provisioning.getOAuthManifestByState(pool, state) : null;
124
+ const row = STATE_RE.test(state) ? await provisioning.getOAuthManifestByState(pool, state) : null;
89
125
  if (!row) {
90
126
  return sendPage(200, callbackPage({ ok: true, title: 'Installed on GitHub',
91
127
  body: '<p>GitHub has installed the app. Go back to your project’s page: it checks the install itself.</p><p><a class="btn" href="/projects?view=mine">Back to my projects →</a></p>' }));
@@ -109,5 +145,78 @@ module.exports = function buildGithubSetupRouter() {
109
145
  }
110
146
  });
111
147
 
148
+ // GET /provisioning/github-app/return?state=<> — where the callback and setup pages send
149
+ // the founder back (task 1004650). The SERVER resolves the one-time state to the project
150
+ // it was minted for and 302s that project's signed-in OWNER to its part 03
151
+ // (/projects?view=new&id=<id>), so the hand-back works from any tab, window or device —
152
+ // nothing in browser storage is read. Not signed in → through the web sign-in and back
153
+ // here. Signed in as anyone else, an unknown or malformed state, or a gone project → one
154
+ // refusal page that names no project (no oracle about which). The state stays single-use
155
+ // where it matters: only the manifest callback's code exchange consumes it, and this route
156
+ // changes nothing — it reveals a project's page only to the person who already owns it.
157
+ // rank: public — as a GATE: it is a browser redirect, so no middleware 401s a signed-out
158
+ // visitor (they are sent through sign-in instead). WHO lands is decided in-handler from
159
+ // the live session: the project's own owner, no Archon override, and nobody else ever
160
+ // learns which project the state names. It reveals no secret and writes nothing.
161
+ router.get('/provisioning/github-app/return', returnRateLimit, async (req, res) => {
162
+ res.set('Cache-Control', 'no-store');
163
+ res.set('Referrer-Policy', 'no-referrer');
164
+ const refuse = () => res.status(404).type('html').send(callbackPage({ ok: false, title: 'This isn’t your project',
165
+ body: '<p>This link is for a project on another account, or it has expired — sign in with the account that started the project, or open your own projects.</p><p><a class="btn" href="/projects?view=mine">My projects →</a></p>' }));
166
+ try {
167
+ const state = String(req.query.state || '');
168
+ if (!STATE_RE.test(state)) return refuse();
169
+ const session = await api.resolveSession(req);
170
+ if (!session) {
171
+ // the relative return passes the web flow's open-redirect guard (safeReturnTo);
172
+ // the repo scope rides every hub sign-in door (ADR 0364)
173
+ return res.redirect(302, `/api/bongos/auth/web/start?return=${encodeURIComponent(returnHref(state))}&repos=1`);
174
+ }
175
+ if (session.status === 'inactive') return refuse();
176
+ const row = await provisioning.getOAuthManifestByState(pool, state);
177
+ const inst = row ? await provisioning.getInstanceById(pool, row.instance_id).catch(() => null) : null;
178
+ if (!ownsLanding(inst, session.builder_id)) return refuse();
179
+ return res.redirect(302, landingHref(inst.id, state));
180
+ } catch (err) {
181
+ log.error('[provisioning] GET /provisioning/github-app/return', err);
182
+ return res.status(500).type('html').send(callbackPage({ ok: false, title: 'Something went wrong',
183
+ body: '<p>We couldn’t find your project just now — it is on your projects page.</p><p><a class="btn" href="/projects?view=mine">My projects →</a></p>' }));
184
+ }
185
+ });
186
+
187
+ // GET /provisioning/github-app/resume[?id=<n>] — the project page's "which project am I
188
+ // connecting?" read when its tab remembers nothing (task 1004650): a hand-back on another
189
+ // device, a fresh tab, a cleared browser. With an id: that project, if the caller owns it.
190
+ // Without: the caller's newest project whose GitHub checklist is not connected yet, made or
191
+ // last tried within a week — so opening the create page later resumes part 03 from the
192
+ // server's view. Either way { project: null } when there is none; never the state token.
193
+ // rank: any authenticated builder (requireBuilder), own projects only — no Archon override.
194
+ router.get('/provisioning/github-app/resume', api.requireBuilder, async (req, res) => {
195
+ try {
196
+ const me = req.builder && req.builder.id;
197
+ const asked = String(req.query.id || '');
198
+ if (asked) {
199
+ if (!/^\d{1,12}$/.test(asked)) return res.json({ ok: true, project: null });
200
+ const inst = await provisioning.getInstanceById(pool, Number(asked)).catch(() => null);
201
+ return res.json({ ok: true, project: ownsLanding(inst, me) ? resumeRecord(inst) : null });
202
+ }
203
+ const now = Date.now();
204
+ const owned = (await provisioning.listInstancesForOwner(pool, me)).filter((i) => ownsLanding(i, me)).slice(0, 5);
205
+ // the (at most five) candidates are read together; the newest unfinished one wins
206
+ const open = await Promise.all(owned.map(async (inst) => {
207
+ const row = await provisioning.getLatestOAuthManifestForInstance(pool, inst.id);
208
+ const touched = Math.max(Date.parse(inst.created_at) || 0, row ? Date.parse(row.created_at) || 0 : 0);
209
+ if (now - touched > RESUME_WINDOW_MS) return false;
210
+ const setup = await githubSetup.githubSetupRead(pool, inst, row, row ? provisioning.publicGithubApp(row).status : null, { withState: false });
211
+ return !setup.checklist.connected;
212
+ }));
213
+ const found = owned.find((_inst, i) => open[i]);
214
+ return res.json({ ok: true, project: found ? resumeRecord(found) : null });
215
+ } catch (err) {
216
+ log.error('[provisioning] GET /provisioning/github-app/resume', err);
217
+ return res.status(500).json({ ok: false, error: 'resume_read_failed' });
218
+ }
219
+ });
220
+
112
221
  return router;
113
222
  };
@@ -1230,11 +1230,22 @@ module.exports = function provisioningRoutes() {
1230
1230
  // GitHub, the owner approves, and GitHub redirects back to the public callback below.
1231
1231
  // The web tier only RECORDS the attempt (state token) — the control-plane runner does
1232
1232
  // the token exchange + cred placement (trust boundary intact, ADR 0111 §2).
1233
- // Body: { account? ('personal'|'org') }.
1233
+ // Body: { account? ('personal'|'org'), org? (a GitHub organisation login) }.
1234
+ // A personal or an organisation account (task 1004651): account 'org' makes the app on
1235
+ // the organisation the founder chose on the page (`org`), else on the repo's owner. The
1236
+ // org name is checked against GitHub's login rules here, never trusted from the page;
1237
+ // whether the founder may make an app there is GitHub's to enforce on its own page
1238
+ // (this module holds no GitHub token to ask with, ADR 0111 §2). Anything other than
1239
+ // account 'org' is the founder's personal account, exactly as before.
1234
1240
  router.post('/provisioning/instances/:id/github-app', auth.requireBuilder, async (req, res) => {
1235
1241
  const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
1236
1242
  if (!id) return;
1237
- if (validateOrRespond(req, res, { account: { type: 'string', maxLength: 12 } })) return;
1243
+ if (validateOrRespond(req, res, { account: { type: 'string', maxLength: 12 }, org: { type: 'string', maxLength: 39 } })) return;
1244
+ const wantsOrg = !!(req.body && req.body.account === 'org');
1245
+ const typedOrg = wantsOrg && typeof req.body.org === 'string' ? req.body.org.trim() : '';
1246
+ if (typedOrg && !githubSetup.validGithubLogin(typedOrg)) {
1247
+ return res.fail('bad_org', { status: 400, message: 'That isn’t a GitHub organisation name: letters, numbers and single dashes, up to 39.' });
1248
+ }
1238
1249
  try {
1239
1250
  const inst = await provisioning.getInstanceById(pool, id);
1240
1251
  if (!inst || String(inst.owner_builder_id) !== String(req.builder.id)) {
@@ -1249,6 +1260,14 @@ module.exports = function provisioningRoutes() {
1249
1260
  if (!origin) {
1250
1261
  return res.fail('mothership_origin_unset', { status: 500, message: 'This control plane has no publicOrigin configured — cannot build the GitHub redirect URL.' });
1251
1262
  }
1263
+ // the org candidate: the one the founder chose, else the repo's owner (a login only)
1264
+ const repoOwner = ownerFromTargetRef(inst.target_ref);
1265
+ const orgLogin = typedOrg || (githubSetup.validGithubLogin(repoOwner) ? repoOwner : null);
1266
+ // an explicit organisation choice never falls back to the personal account unseen
1267
+ if (wantsOrg && !orgLogin) {
1268
+ return res.fail('org_required', { status: 400, message: 'Name the GitHub organisation to make the app on.' });
1269
+ }
1270
+ const account = wantsOrg ? 'org' : 'personal';
1252
1271
  const state = crypto.randomBytes(32).toString('hex');
1253
1272
  const expiresAt = new Date(Date.now() + MANIFEST_STATE_TTL_MS).toISOString();
1254
1273
  await provisioning.createOAuthManifest(pool, { instanceId: id, state, requestedBy: req.builder.id, expiresAt });
@@ -1256,13 +1275,19 @@ module.exports = function provisioningRoutes() {
1256
1275
  instanceId: id, ownerBuilderId: req.builder.id, event: 'oauth-manifest-start',
1257
1276
  detail: 'GitHub App Manifest link minted', actor: 'api:self',
1258
1277
  }).catch(() => {});
1278
+ // which account owns this attempt's app, so the page can link its settings there
1279
+ // from any tab or device (github-setup.js reads it back as app_org)
1280
+ if (account === 'org') {
1281
+ await provisioning.recordEvent(pool, {
1282
+ instanceId: id, ownerBuilderId: req.builder.id, event: githubSetup.SETUP_EVENTS.appOrg,
1283
+ detail: orgLogin, actor: 'api:self',
1284
+ }).catch(() => {});
1285
+ }
1259
1286
 
1260
1287
  const manifest = provisioning.buildGithubAppManifest({
1261
1288
  instanceName: inst.slug, slug: inst.slug, instanceDomain: inst.domain,
1262
1289
  mothershipOrigin: origin, brand: mothershipBrand(),
1263
1290
  });
1264
- const orgLogin = ownerFromTargetRef(inst.target_ref);
1265
- const account = req.body && req.body.account === 'org' && orgLogin ? 'org' : 'personal';
1266
1291
  res.json({
1267
1292
  state,
1268
1293
  account,