@aglyn/plugins-marketplace 1.15.0 → 1.15.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/README.md +1 -0
- package/package.json +15 -14
- package/src/lib/components/editor-connections-provider.component.d.ts +37 -0
- package/src/lib/components/editor-connections-provider.component.js +161 -0
- package/src/lib/components/editor-connections-provider.component.js.map +1 -0
- package/src/lib/components/listing-connection-credentials-card.component.d.ts +33 -0
- package/src/lib/components/listing-connection-credentials-card.component.js +318 -0
- package/src/lib/components/listing-connection-credentials-card.component.js.map +1 -0
- package/src/lib/components/listing-detail-page.component.js +10 -2
- package/src/lib/components/listing-detail-page.component.js.map +1 -1
- package/src/lib/components/site-plugin-connections-card.component.d.ts +38 -0
- package/src/lib/components/site-plugin-connections-card.component.js +576 -0
- package/src/lib/components/site-plugin-connections-card.component.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +17 -0
- package/src/lib/declarations.console-server.js +41 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.js +11 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +68 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-plugin-connections-api.d.ts +89 -0
- package/src/lib/hooks/use-plugin-connections-api.js +132 -0
- package/src/lib/hooks/use-plugin-connections-api.js.map +1 -0
- package/src/lib/model/editor-connections.d.ts +46 -0
- package/src/lib/model/editor-connections.js +70 -0
- package/src/lib/model/editor-connections.js.map +1 -0
- package/src/lib/model/listing-price-floor.js +7 -4
- package/src/lib/model/listing-price-floor.js.map +1 -1
- package/src/lib/model/marketplace-publisher-permissions.d.ts +3 -1
- package/src/lib/model/marketplace-publisher-permissions.js +14 -0
- package/src/lib/model/marketplace-publisher-permissions.js.map +1 -1
- package/src/lib/model/marketplace-site-permissions.d.ts +9 -0
- package/src/lib/model/marketplace-site-permissions.js +29 -0
- package/src/lib/model/marketplace-site-permissions.js.map +1 -1
- package/src/lib/model/plugin-connection-card.d.ts +88 -0
- package/src/lib/model/plugin-connection-card.js +187 -0
- package/src/lib/model/plugin-connection-card.js.map +1 -0
- package/src/lib/model/plugin-connections.d.ts +172 -0
- package/src/lib/model/plugin-connections.js +119 -0
- package/src/lib/model/plugin-connections.js.map +1 -0
- package/src/lib/plugin.js +21 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/admin-reviews.js +18 -12
- package/src/lib/server/admin-reviews.js.map +1 -1
- package/src/lib/server/artifact-inventory.js +3 -1
- package/src/lib/server/artifact-inventory.js.map +1 -1
- package/src/lib/server/checkout.js +4 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/connections/cleanup.d.ts +84 -0
- package/src/lib/server/connections/cleanup.js +116 -0
- package/src/lib/server/connections/cleanup.js.map +1 -0
- package/src/lib/server/connections/config.d.ts +42 -0
- package/src/lib/server/connections/config.js +48 -0
- package/src/lib/server/connections/config.js.map +1 -0
- package/src/lib/server/connections/connection-store.d.ts +173 -0
- package/src/lib/server/connections/connection-store.js +331 -0
- package/src/lib/server/connections/connection-store.js.map +1 -0
- package/src/lib/server/connections/credentials-store.d.ts +71 -0
- package/src/lib/server/connections/credentials-store.js +78 -0
- package/src/lib/server/connections/credentials-store.js.map +1 -0
- package/src/lib/server/connections/erasure.d.ts +25 -0
- package/src/lib/server/connections/erasure.js +72 -0
- package/src/lib/server/connections/erasure.js.map +1 -0
- package/src/lib/server/connections/installed-connection.d.ts +64 -0
- package/src/lib/server/connections/installed-connection.js +92 -0
- package/src/lib/server/connections/installed-connection.js.map +1 -0
- package/src/lib/server/connections/oauth-state.d.ts +129 -0
- package/src/lib/server/connections/oauth-state.js +252 -0
- package/src/lib/server/connections/oauth-state.js.map +1 -0
- package/src/lib/server/connections/provider-http.d.ts +99 -0
- package/src/lib/server/connections/provider-http.js +253 -0
- package/src/lib/server/connections/provider-http.js.map +1 -0
- package/src/lib/server/connections/record-announce.d.ts +20 -0
- package/src/lib/server/connections/record-announce.js +53 -0
- package/src/lib/server/connections/record-announce.js.map +1 -0
- package/src/lib/server/connections/record-mapping.d.ts +63 -0
- package/src/lib/server/connections/record-mapping.js +151 -0
- package/src/lib/server/connections/record-mapping.js.map +1 -0
- package/src/lib/server/connections/record-routes.d.ts +17 -0
- package/src/lib/server/connections/record-routes.js +96 -0
- package/src/lib/server/connections/record-routes.js.map +1 -0
- package/src/lib/server/connections/revoke.d.ts +29 -0
- package/src/lib/server/connections/revoke.js +52 -0
- package/src/lib/server/connections/revoke.js.map +1 -0
- package/src/lib/server/connections/routes.d.ts +77 -0
- package/src/lib/server/connections/routes.js +706 -0
- package/src/lib/server/connections/routes.js.map +1 -0
- package/src/lib/server/connections/site-records-platform.d.ts +24 -0
- package/src/lib/server/connections/site-records-platform.js +94 -0
- package/src/lib/server/connections/site-records-platform.js.map +1 -0
- package/src/lib/server/connections/site-records.d.ts +95 -0
- package/src/lib/server/connections/site-records.js +221 -0
- package/src/lib/server/connections/site-records.js.map +1 -0
- package/src/lib/server/connections/sync.d.ts +110 -0
- package/src/lib/server/connections/sync.js +414 -0
- package/src/lib/server/connections/sync.js.map +1 -0
- package/src/lib/server/install-plugin.js +56 -6
- package/src/lib/server/install-plugin.js.map +1 -1
- package/src/lib/server/publish-plugin.js +5 -2
- package/src/lib/server/publish-plugin.js.map +1 -1
- package/src/lib/server/revenue-source.js +6 -5
- package/src/lib/server/revenue-source.js.map +1 -1
- package/src/lib/server.js +60 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +34 -0
- package/src/lib/testing/memory-firestore.js +250 -0
- package/src/lib/testing/memory-firestore.js.map +1 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { holdsStampedSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
18
|
+
import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
19
|
+
import { isPluginConnectionChoiceId, pluginConnectionAuthorizeUrl } from "@aglyn/aglyn/app-utils/plugin-connections";
|
|
20
|
+
import { firebaseAdmin, getOrgForHost, logHostActivity } from "@aglyn/tenant-data-admin";
|
|
21
|
+
import { isEmailVerified, isImpersonationSession } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
22
|
+
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
23
|
+
import { PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION, PLUGIN_CONNECTIONS_FEATURE, PLUGIN_CONNECTIONS_PLAN_REFUSAL, PLUGIN_CONNECTIONS_SITE_PERMISSION, PLUGIN_CONNECTIONS_SITE_ROLE_PRESET, pluginConnectionHosts, pluginConnectionsPermissionRefusal } from "../../model/plugin-connections.js";
|
|
24
|
+
import { publisherPermissionVerdict } from "../publisher-permissions.js";
|
|
25
|
+
import { isHostId, isListingId, readPluginConnectionsKeyring } from "./config.js";
|
|
26
|
+
import { announceConnectionRecordChange } from "./record-announce.js";
|
|
27
|
+
import { connectionAccessToken, connectionReady, connectionRecordRef, connectionRef, getConnection, listHostConnections, newConnectionRecord, PluginConnectionReconnectRequiredError, toConnectionView } from "./connection-store.js";
|
|
28
|
+
import { clearCredentials, credentialsStatus, openCredentials, saveCredentials, sealCredentials } from "./credentials-store.js";
|
|
29
|
+
import { installedConnection, installedConnectionsForHost, latestListingConnections } from "./installed-connection.js";
|
|
30
|
+
import { connectRedirectUri, consoleOriginFor, consolePath, consumeConnectState, mintConnectState, readConnectState, recordConnectState } from "./oauth-state.js";
|
|
31
|
+
import { exchangePluginConnectionCode, listPluginConnectionResource, pluginConnectionRuntime, PluginConnectionProviderError } from "./provider-http.js";
|
|
32
|
+
import { revokeRecord } from "./revoke.js";
|
|
33
|
+
import { runConnectionSync } from "./sync.js";
|
|
34
|
+
/**
|
|
35
|
+
* THE DECLARED CONNECTIONS' ROUTES (AGL-4193), served by the console for every
|
|
36
|
+
* marketplace plugin that declares a connection:
|
|
37
|
+
*
|
|
38
|
+
* GET|POST plugin-connections/connect/start the provider's consent address (site key, plan)
|
|
39
|
+
* GET plugin-connections/connect/callback the provider's redirect back (signed state, no bearer)
|
|
40
|
+
* GET plugin-connections/resources what the grant reaches, to pick (site key, plan)
|
|
41
|
+
* POST plugin-connections/choose the resource the site reads (site key, plan)
|
|
42
|
+
* POST plugin-connections/disconnect revoke and forget the grant (site key, any plan)
|
|
43
|
+
* GET plugin-connections/status each declared connection, no token (site key)
|
|
44
|
+
* POST plugin-connections/sync run the declared sync once ("Sync now") (site key, plan)
|
|
45
|
+
* GET|POST plugin-connections/credentials the listing's OAuth app, write-only (publisher key)
|
|
46
|
+
*
|
|
47
|
+
* The site key is "Manage a site's plugin connections"
|
|
48
|
+
* (`marketplace.siteConnections`) and the publisher key "Set a listing's
|
|
49
|
+
* connection credentials" (`marketplace.connectionCredentials`); a refusal
|
|
50
|
+
* names the permission, never a role. The callback path is registered as a
|
|
51
|
+
* redirect URI at providers and must never move.
|
|
52
|
+
*
|
|
53
|
+
* Without `PLUGIN_CONNECTIONS_TOKEN_KEY` every route that would seal or open a
|
|
54
|
+
* secret answers 503, and status says `configured: false`.
|
|
55
|
+
*/ const NO_STORE = {
|
|
56
|
+
'Cache-Control': 'no-store'
|
|
57
|
+
};
|
|
58
|
+
const MAX_CODE_CHARS = 4096;
|
|
59
|
+
const CLIENT_ID_PATTERN = /^[\x21-\x7e]{1,512}$/;
|
|
60
|
+
const CLIENT_SECRET_PATTERN = /^[\x21-\x7e]{1,1024}$/;
|
|
61
|
+
const NOT_CONFIGURED = 'Plugin connections are not set up on this deployment yet.';
|
|
62
|
+
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
63
|
+
export function routeJson(body, status = 200) {
|
|
64
|
+
return Response.json(body, {
|
|
65
|
+
status,
|
|
66
|
+
headers: NO_STORE
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function routeError(status, error, extra = {}) {
|
|
70
|
+
return Response.json(_extends({
|
|
71
|
+
error
|
|
72
|
+
}, extra), {
|
|
73
|
+
status,
|
|
74
|
+
headers: NO_STORE
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const methodNotAllowed = (allow)=>new Response(JSON.stringify({
|
|
78
|
+
error: 'Method not allowed'
|
|
79
|
+
}), {
|
|
80
|
+
status: 405,
|
|
81
|
+
headers: _extends({
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
Allow: allow
|
|
84
|
+
}, NO_STORE)
|
|
85
|
+
});
|
|
86
|
+
/** The request's parameters: the query for a GET, the JSON body (over the query) for a POST. */ async function readParams(request) {
|
|
87
|
+
const query = Object.fromEntries(new URL(request.url).searchParams.entries());
|
|
88
|
+
if (request.method === 'GET' || request.method === 'HEAD') return query;
|
|
89
|
+
const body = await request.json().catch(()=>null);
|
|
90
|
+
return body && typeof body === 'object' && !Array.isArray(body) ? _extends({}, query, body) : query;
|
|
91
|
+
}
|
|
92
|
+
/** The signed-in member, or the refusal. */ async function signedIn(request) {
|
|
93
|
+
var _request_headers_get;
|
|
94
|
+
const authorization = (_request_headers_get = request.headers.get('authorization')) != null ? _request_headers_get : '';
|
|
95
|
+
if (!authorization.startsWith('Bearer ')) return routeError(401, 'Unauthenticated');
|
|
96
|
+
let decoded;
|
|
97
|
+
try {
|
|
98
|
+
decoded = await firebaseAdmin.app().auth().verifyIdToken(authorization.slice('Bearer '.length));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Only a token the verifier refused is the caller's fault; a certificate
|
|
101
|
+
// fetch that failed is ours, and must not read as a bad sign-in.
|
|
102
|
+
if (isRefusedIdToken(error)) return routeError(401, 'Unauthenticated');
|
|
103
|
+
console.error('plugin-connections: the sign-in could not be checked', error);
|
|
104
|
+
return routeError(503, 'The sign-in could not be checked. Try again.');
|
|
105
|
+
}
|
|
106
|
+
if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {
|
|
107
|
+
return routeError(403, 'Verify your email address first');
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
uid: decoded.uid,
|
|
111
|
+
staff: decoded['staff'] === true
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Whether `uid` holds "Manage a site's plugin connections" on the site. */ export function holdsSiteConnectionsKey(host, uid) {
|
|
115
|
+
return holdsStampedSiteKey(host, uid, PLUGIN_CONNECTIONS_SITE_PERMISSION, PLUGIN_CONNECTIONS_SITE_ROLE_PRESET);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* THE GATE EVERY SITE ROUTE CLIMBS. The console's dispatcher has already
|
|
119
|
+
* refused a site with the marketplace switched off or unreleased, a lockdown
|
|
120
|
+
* and a caller over the write limit. What it cannot know is the member, the
|
|
121
|
+
* key and the plan:
|
|
122
|
+
*
|
|
123
|
+
* 401 no bearer token, or one the verifier refused
|
|
124
|
+
* 403 an unverified address (an impersonation session is exempt)
|
|
125
|
+
* 400 no site named
|
|
126
|
+
* 404 no such site
|
|
127
|
+
* 403 without "Manage a site's plugin connections" on the site — held by
|
|
128
|
+
* the member themselves: the grant is the business's own account at
|
|
129
|
+
* the provider, so staff support does not connect one
|
|
130
|
+
* 403 a plan without connections, when the route asks for it
|
|
131
|
+
*/ export async function siteConnectionsGate(request, params, options) {
|
|
132
|
+
var _hostSnapshot_data;
|
|
133
|
+
const caller = await signedIn(request);
|
|
134
|
+
if (caller instanceof Response) return caller;
|
|
135
|
+
const hostId = params['hostId'];
|
|
136
|
+
if (!isHostId(hostId)) return routeError(400, 'Missing hostId');
|
|
137
|
+
const [hostSnapshot, resolved] = await Promise.all([
|
|
138
|
+
firestore().collection('hosts').doc(hostId).get(),
|
|
139
|
+
getOrgForHost(hostId)
|
|
140
|
+
]);
|
|
141
|
+
if (!hostSnapshot.exists || !resolved) return routeError(404, 'Site not found');
|
|
142
|
+
const host = (_hostSnapshot_data = hostSnapshot.data()) != null ? _hostSnapshot_data : {};
|
|
143
|
+
if (!holdsSiteConnectionsKey(host, caller.uid)) {
|
|
144
|
+
return routeError(403, pluginConnectionsPermissionRefusal(PLUGIN_CONNECTIONS_SITE_PERMISSION), {
|
|
145
|
+
reason: 'permission',
|
|
146
|
+
permission: PLUGIN_CONNECTIONS_SITE_PERMISSION
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const planAllows = checkEntitlement(resolved.org, PLUGIN_CONNECTIONS_FEATURE);
|
|
150
|
+
if (options.requirePlan && !planAllows) {
|
|
151
|
+
return routeError(403, PLUGIN_CONNECTIONS_PLAN_REFUSAL, {
|
|
152
|
+
reason: 'plan',
|
|
153
|
+
feature: PLUGIN_CONNECTIONS_FEATURE
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
uid: caller.uid,
|
|
158
|
+
hostId,
|
|
159
|
+
orgId: resolved.orgId,
|
|
160
|
+
org: resolved.org,
|
|
161
|
+
host,
|
|
162
|
+
planAllows
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** The keyring, or the 503 that says connections are not set up. */ function keyringOr503() {
|
|
166
|
+
var _readPluginConnectionsKeyring;
|
|
167
|
+
return (_readPluginConnectionsKeyring = readPluginConnectionsKeyring()) != null ? _readPluginConnectionsKeyring : routeError(503, NOT_CONFIGURED);
|
|
168
|
+
}
|
|
169
|
+
/** The declared connection a request names on the actor's site, with the publisher's app, or the refusal. */ async function resolveConnection(actor, params, keyring) {
|
|
170
|
+
const listingId = params['listingId'];
|
|
171
|
+
const connectionId = params['connectionId'];
|
|
172
|
+
if (!isListingId(listingId) || typeof connectionId !== 'string' || !connectionId) {
|
|
173
|
+
return routeError(400, 'Missing listingId or connectionId');
|
|
174
|
+
}
|
|
175
|
+
const found = await installedConnection(firestore(), {
|
|
176
|
+
hostId: actor.hostId,
|
|
177
|
+
orgId: actor.orgId,
|
|
178
|
+
listingId,
|
|
179
|
+
connectionId
|
|
180
|
+
});
|
|
181
|
+
if (!found) return routeError(404, 'This site runs no plugin that declares that connection.');
|
|
182
|
+
let client;
|
|
183
|
+
try {
|
|
184
|
+
client = await openCredentials(firestore(), keyring, listingId, connectionId);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('plugin-connections: a listing’s credentials did not open', error);
|
|
187
|
+
client = null;
|
|
188
|
+
}
|
|
189
|
+
if (!client) {
|
|
190
|
+
return routeError(409, 'The plugin’s publisher has not finished setting up this connection yet.', {
|
|
191
|
+
reason: 'credentials'
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
listingId,
|
|
196
|
+
displayName: found.installed.displayName,
|
|
197
|
+
version: found.installed.version,
|
|
198
|
+
connection: found.connection,
|
|
199
|
+
client
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const activityTarget = (connection, listingId)=>({
|
|
203
|
+
type: 'marketplace:plugin-connection',
|
|
204
|
+
id: `${listingId}/${connection.id}`,
|
|
205
|
+
name: connection.label
|
|
206
|
+
});
|
|
207
|
+
/* ------------------------------------------------------------------ */ /* Connect */ /* ------------------------------------------------------------------ */ export async function connectStartRoute(request) {
|
|
208
|
+
if (request.method !== 'GET' && request.method !== 'POST') return methodNotAllowed('GET, POST');
|
|
209
|
+
const params = await readParams(request);
|
|
210
|
+
const actor = await siteConnectionsGate(request, params, {
|
|
211
|
+
requirePlan: true
|
|
212
|
+
});
|
|
213
|
+
if (actor instanceof Response) return actor;
|
|
214
|
+
const keyring = keyringOr503();
|
|
215
|
+
if (keyring instanceof Response) return keyring;
|
|
216
|
+
const resolved = await resolveConnection(actor, params, keyring);
|
|
217
|
+
if (resolved instanceof Response) return resolved;
|
|
218
|
+
const origin = consoleOriginFor(request.url);
|
|
219
|
+
const redirectUri = connectRedirectUri(request.url);
|
|
220
|
+
if (!origin || !redirectUri) return routeError(503, 'This console has no address to come back to.');
|
|
221
|
+
const returnTo = consolePath(params['returnTo'], origin);
|
|
222
|
+
if (!returnTo) return routeError(400, 'Connect from the console.');
|
|
223
|
+
const nowMs = pluginConnectionRuntime.now();
|
|
224
|
+
let minted;
|
|
225
|
+
try {
|
|
226
|
+
minted = mintConnectState({
|
|
227
|
+
hostId: actor.hostId,
|
|
228
|
+
orgId: actor.orgId,
|
|
229
|
+
uid: actor.uid,
|
|
230
|
+
listingId: resolved.listingId,
|
|
231
|
+
connectionId: resolved.connection.id,
|
|
232
|
+
returnTo,
|
|
233
|
+
nowMs
|
|
234
|
+
});
|
|
235
|
+
} catch (unused) {
|
|
236
|
+
return routeError(503, NOT_CONFIGURED);
|
|
237
|
+
}
|
|
238
|
+
await recordConnectState(firestore(), {
|
|
239
|
+
claims: minted.claims,
|
|
240
|
+
redirectUri,
|
|
241
|
+
nowMs
|
|
242
|
+
});
|
|
243
|
+
return routeJson({
|
|
244
|
+
authorizeUrl: pluginConnectionAuthorizeUrl(resolved.connection, {
|
|
245
|
+
clientId: resolved.client.clientId,
|
|
246
|
+
redirectUri,
|
|
247
|
+
state: minted.state
|
|
248
|
+
})
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** The workspace and member a provider's redirect is for, read off the signed state. */ export function connectCallbackSubject(request) {
|
|
252
|
+
const read = readConnectState(new URL(request.url).searchParams.get('state'), pluginConnectionRuntime.now());
|
|
253
|
+
if (read.ok) return {
|
|
254
|
+
orgId: read.claims.orgId,
|
|
255
|
+
uid: read.claims.uid
|
|
256
|
+
};
|
|
257
|
+
if (read.ok === false && read.refusal === 'state-expired') {
|
|
258
|
+
const { claims } = read;
|
|
259
|
+
return {
|
|
260
|
+
orgId: claims.orgId,
|
|
261
|
+
uid: claims.uid
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Whether the member who started the connect may still finish it: the site
|
|
268
|
+
* still in the same workspace, on a plan with connections, and the member
|
|
269
|
+
* still holding the key.
|
|
270
|
+
*/ async function mayStillConnect(claims) {
|
|
271
|
+
var _hostSnapshot_data;
|
|
272
|
+
const [hostSnapshot, resolved] = await Promise.all([
|
|
273
|
+
firestore().collection('hosts').doc(claims.hostId).get(),
|
|
274
|
+
getOrgForHost(claims.hostId)
|
|
275
|
+
]);
|
|
276
|
+
if (!hostSnapshot.exists || !resolved || resolved.orgId !== claims.orgId) return null;
|
|
277
|
+
const host = (_hostSnapshot_data = hostSnapshot.data()) != null ? _hostSnapshot_data : {};
|
|
278
|
+
if (!holdsSiteConnectionsKey(host, claims.uid)) return null;
|
|
279
|
+
if (!checkEntitlement(resolved.org, PLUGIN_CONNECTIONS_FEATURE)) return null;
|
|
280
|
+
return {
|
|
281
|
+
uid: claims.uid,
|
|
282
|
+
hostId: claims.hostId,
|
|
283
|
+
orgId: resolved.orgId,
|
|
284
|
+
org: resolved.org,
|
|
285
|
+
host,
|
|
286
|
+
planAllows: true
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const plain = (status, text)=>new Response(text, {
|
|
290
|
+
status,
|
|
291
|
+
headers: _extends({
|
|
292
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
293
|
+
'Referrer-Policy': 'no-referrer'
|
|
294
|
+
}, NO_STORE)
|
|
295
|
+
});
|
|
296
|
+
/** Back to the console page the connect started on, with what happened. */ function backTo(returnTo, claims, outcome) {
|
|
297
|
+
const url = new URL(returnTo, 'https://console.invalid');
|
|
298
|
+
url.searchParams.set('pluginConnectionListing', claims.listingId);
|
|
299
|
+
url.searchParams.set('pluginConnection', claims.connectionId);
|
|
300
|
+
url.searchParams.set('pluginConnectionResult', outcome);
|
|
301
|
+
return new Response(null, {
|
|
302
|
+
status: 303,
|
|
303
|
+
headers: _extends({
|
|
304
|
+
// A RELATIVE location: the callback's own origin, which is the console's.
|
|
305
|
+
Location: `${url.pathname}${url.search}`,
|
|
306
|
+
'Referrer-Policy': 'no-referrer'
|
|
307
|
+
}, NO_STORE)
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
export async function connectCallbackRoute(request) {
|
|
311
|
+
var _params_get;
|
|
312
|
+
if (request.method !== 'GET') return methodNotAllowed('GET');
|
|
313
|
+
const params = new URL(request.url).searchParams;
|
|
314
|
+
const nowMs = pluginConnectionRuntime.now();
|
|
315
|
+
const read = readConnectState(params.get('state'), nowMs);
|
|
316
|
+
if (read.ok === false && read.refusal === 'state-invalid') {
|
|
317
|
+
return plain(400, 'This connection link is not valid. Return to the console and connect again.');
|
|
318
|
+
}
|
|
319
|
+
const claims = read.claims;
|
|
320
|
+
const origin = consoleOriginFor(request.url);
|
|
321
|
+
const returnTo = origin ? consolePath(claims.returnTo, origin) : null;
|
|
322
|
+
if (!returnTo) return plain(400, 'This connection link is not valid. Return to the console and connect again.');
|
|
323
|
+
if (read.ok === false) return backTo(returnTo, claims, 'expired');
|
|
324
|
+
const consumed = await consumeConnectState(firestore(), {
|
|
325
|
+
claims,
|
|
326
|
+
nowMs
|
|
327
|
+
});
|
|
328
|
+
if (consumed.ok === false) return backTo(returnTo, claims, 'expired');
|
|
329
|
+
if (params.get('error')) return backTo(returnTo, claims, 'denied');
|
|
330
|
+
const code = (_params_get = params.get('code')) != null ? _params_get : '';
|
|
331
|
+
if (!code || code.length > MAX_CODE_CHARS) return backTo(returnTo, claims, 'failed');
|
|
332
|
+
const actor = await mayStillConnect(claims);
|
|
333
|
+
if (!actor) return backTo(returnTo, claims, 'not-permitted');
|
|
334
|
+
const keyring = readPluginConnectionsKeyring();
|
|
335
|
+
if (!keyring) return backTo(returnTo, claims, 'failed');
|
|
336
|
+
const resolved = await resolveConnection(actor, claims, keyring);
|
|
337
|
+
if (resolved instanceof Response) return backTo(returnTo, claims, resolved.status === 409 ? 'not-set-up' : 'failed');
|
|
338
|
+
try {
|
|
339
|
+
const tokens = await exchangePluginConnectionCode(resolved.connection, resolved.client, {
|
|
340
|
+
code,
|
|
341
|
+
redirectUri: consumed.redirectUri
|
|
342
|
+
});
|
|
343
|
+
const previous = await getConnection(firestore(), actor.hostId, resolved.listingId, resolved.connection.id);
|
|
344
|
+
const record = newConnectionRecord({
|
|
345
|
+
keyring,
|
|
346
|
+
hostId: actor.hostId,
|
|
347
|
+
orgId: actor.orgId,
|
|
348
|
+
listingId: resolved.listingId,
|
|
349
|
+
connection: resolved.connection,
|
|
350
|
+
tokens,
|
|
351
|
+
uid: actor.uid,
|
|
352
|
+
nowMs,
|
|
353
|
+
previous
|
|
354
|
+
});
|
|
355
|
+
await connectionRef(firestore(), actor.hostId, resolved.listingId, resolved.connection.id).set(record);
|
|
356
|
+
await logHostActivity(actor.hostId, {
|
|
357
|
+
uid: actor.uid
|
|
358
|
+
}, `Connected ${resolved.connection.label} for ${resolved.displayName}`, activityTarget(resolved.connection, resolved.listingId));
|
|
359
|
+
return backTo(returnTo, claims, 'connected');
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error('plugin-connections: connect callback failed', error instanceof Error ? error.message : error);
|
|
362
|
+
return backTo(returnTo, claims, 'failed');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/* ------------------------------------------------------------------ */ /* Resources */ /* ------------------------------------------------------------------ */ /** The site's connection behind the gate, the plan, the key and the declaration. */ async function connectedRoute(request, method) {
|
|
366
|
+
if (request.method !== method) return methodNotAllowed(method);
|
|
367
|
+
const params = await readParams(request);
|
|
368
|
+
const actor = await siteConnectionsGate(request, params, {
|
|
369
|
+
requirePlan: true
|
|
370
|
+
});
|
|
371
|
+
if (actor instanceof Response) return actor;
|
|
372
|
+
const keyring = keyringOr503();
|
|
373
|
+
if (keyring instanceof Response) return keyring;
|
|
374
|
+
const resolved = await resolveConnection(actor, params, keyring);
|
|
375
|
+
if (resolved instanceof Response) return resolved;
|
|
376
|
+
const record = await getConnection(firestore(), actor.hostId, resolved.listingId, resolved.connection.id);
|
|
377
|
+
if (!record) return routeError(409, `${resolved.connection.label} is not connected.`, {
|
|
378
|
+
reason: 'not-connected'
|
|
379
|
+
});
|
|
380
|
+
if (record.status === 'reconnect_required') {
|
|
381
|
+
return routeError(409, `${resolved.connection.label} needs to be connected again.`, {
|
|
382
|
+
reason: 'reconnect'
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
params,
|
|
387
|
+
actor,
|
|
388
|
+
keyring,
|
|
389
|
+
listingId: resolved.listingId,
|
|
390
|
+
version: resolved.version,
|
|
391
|
+
connection: resolved.connection,
|
|
392
|
+
client: resolved.client,
|
|
393
|
+
record
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/** A resource's items, refreshing the token once on a refusal. */ async function listResource(context, resourceId) {
|
|
397
|
+
var _connection_resources, _connection_resources1, _connection_resources2;
|
|
398
|
+
const { connection, record } = context;
|
|
399
|
+
const index = ((_connection_resources = connection.resources) != null ? _connection_resources : []).findIndex((resource)=>resource.id === resourceId);
|
|
400
|
+
if (index < 0) return routeError(404, 'That connection declares no such resource.');
|
|
401
|
+
const resource = ((_connection_resources1 = connection.resources) != null ? _connection_resources1 : [])[index];
|
|
402
|
+
const earlier = ((_connection_resources2 = connection.resources) != null ? _connection_resources2 : []).slice(0, index);
|
|
403
|
+
const missing = earlier.find((one)=>!record.choices[one.id]);
|
|
404
|
+
if (missing) return routeError(409, `Choose the ${missing.label.toLowerCase()} first.`, {
|
|
405
|
+
reason: 'choose-first',
|
|
406
|
+
resource: missing.id
|
|
407
|
+
});
|
|
408
|
+
const choices = Object.fromEntries(earlier.map((one)=>[
|
|
409
|
+
one.id,
|
|
410
|
+
record.choices[one.id].id
|
|
411
|
+
]));
|
|
412
|
+
const deps = {
|
|
413
|
+
firestore: firestore(),
|
|
414
|
+
keyring: context.keyring,
|
|
415
|
+
connection,
|
|
416
|
+
client: context.client,
|
|
417
|
+
now: pluginConnectionRuntime.now,
|
|
418
|
+
sleep: pluginConnectionRuntime.sleep
|
|
419
|
+
};
|
|
420
|
+
try {
|
|
421
|
+
let accessToken = await connectionAccessToken(deps, record);
|
|
422
|
+
try {
|
|
423
|
+
return {
|
|
424
|
+
items: await listPluginConnectionResource(connection, resource, {
|
|
425
|
+
accessToken,
|
|
426
|
+
choices
|
|
427
|
+
})
|
|
428
|
+
};
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (!(error instanceof PluginConnectionProviderError) || error.code !== 'auth') throw error;
|
|
431
|
+
accessToken = await connectionAccessToken(deps, record, {
|
|
432
|
+
force: true
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
items: await listPluginConnectionResource(connection, resource, {
|
|
436
|
+
accessToken,
|
|
437
|
+
choices
|
|
438
|
+
})
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof PluginConnectionReconnectRequiredError) {
|
|
443
|
+
return routeError(409, `${connection.label} needs to be connected again.`, {
|
|
444
|
+
reason: 'reconnect'
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
if (error instanceof PluginConnectionProviderError) {
|
|
448
|
+
return routeError(502, `${connection.label} did not answer: ${error.message}`, {
|
|
449
|
+
reason: 'provider'
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export async function resourcesRoute(request) {
|
|
456
|
+
var _context_params_resource;
|
|
457
|
+
const context = await connectedRoute(request, 'GET');
|
|
458
|
+
if (context instanceof Response) return context;
|
|
459
|
+
const listed = await listResource(context, String((_context_params_resource = context.params['resource']) != null ? _context_params_resource : ''));
|
|
460
|
+
return listed instanceof Response ? listed : routeJson(listed);
|
|
461
|
+
}
|
|
462
|
+
export async function chooseRoute(request) {
|
|
463
|
+
var _params_resource, _connection_resources;
|
|
464
|
+
var _record_choices_resourceId;
|
|
465
|
+
const context = await connectedRoute(request, 'POST');
|
|
466
|
+
if (context instanceof Response) return context;
|
|
467
|
+
const { params, actor, connection, record, listingId } = context;
|
|
468
|
+
const resourceId = String((_params_resource = params['resource']) != null ? _params_resource : '');
|
|
469
|
+
const id = params['id'];
|
|
470
|
+
if (!isPluginConnectionChoiceId(id)) return routeError(400, 'Choose one of the listed items.');
|
|
471
|
+
// Re-read what the grant reaches: a choice is only ever one the provider listed.
|
|
472
|
+
const listed = await listResource(context, resourceId);
|
|
473
|
+
if (listed instanceof Response) return listed;
|
|
474
|
+
const choice = listed.items.find((item)=>item.id === id);
|
|
475
|
+
if (!choice) return routeError(400, 'That is not one this connection reaches.');
|
|
476
|
+
const resources = (_connection_resources = connection.resources) != null ? _connection_resources : [];
|
|
477
|
+
const index = resources.findIndex((resource)=>resource.id === resourceId);
|
|
478
|
+
// A new choice clears every later one: a location belongs to its account.
|
|
479
|
+
const choices = _extends({}, record.choices, {
|
|
480
|
+
[resourceId]: choice
|
|
481
|
+
});
|
|
482
|
+
if (((_record_choices_resourceId = record.choices[resourceId]) == null ? void 0 : _record_choices_resourceId.id) !== choice.id) {
|
|
483
|
+
for (const later of resources.slice(index + 1))delete choices[later.id];
|
|
484
|
+
}
|
|
485
|
+
const nowMs = pluginConnectionRuntime.now();
|
|
486
|
+
const ready = connectionReady({
|
|
487
|
+
choices
|
|
488
|
+
}, connection);
|
|
489
|
+
await connectionRef(firestore(), actor.hostId, listingId, connection.id).update({
|
|
490
|
+
choices,
|
|
491
|
+
nextSyncAtMs: ready ? nowMs : null,
|
|
492
|
+
updatedAtMs: nowMs
|
|
493
|
+
});
|
|
494
|
+
const next = await getConnection(firestore(), actor.hostId, listingId, connection.id);
|
|
495
|
+
return routeJson({
|
|
496
|
+
connection: next ? toConnectionView(next, connection) : null
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/* ------------------------------------------------------------------ */ /* Disconnect and status */ /* ------------------------------------------------------------------ */ export async function disconnectRoute(request) {
|
|
500
|
+
if (request.method !== 'POST') return methodNotAllowed('POST');
|
|
501
|
+
const params = await readParams(request);
|
|
502
|
+
// Any plan: a workspace that moved to Free must still be able to let go.
|
|
503
|
+
const actor = await siteConnectionsGate(request, params, {
|
|
504
|
+
requirePlan: false
|
|
505
|
+
});
|
|
506
|
+
if (actor instanceof Response) return actor;
|
|
507
|
+
const listingId = params['listingId'];
|
|
508
|
+
const connectionId = params['connectionId'];
|
|
509
|
+
if (!isListingId(listingId) || typeof connectionId !== 'string' || !connectionId) {
|
|
510
|
+
return routeError(400, 'Missing listingId or connectionId');
|
|
511
|
+
}
|
|
512
|
+
const record = await getConnection(firestore(), actor.hostId, listingId, connectionId);
|
|
513
|
+
if (!record) return routeJson({
|
|
514
|
+
connection: null
|
|
515
|
+
});
|
|
516
|
+
const revoked = await revokeRecord(record, actor.orgId);
|
|
517
|
+
// The record goes with the grant (AGL-4194): what was read with a grant the
|
|
518
|
+
// owner withdrew is not shown on their site afterwards.
|
|
519
|
+
await Promise.all([
|
|
520
|
+
connectionRef(firestore(), actor.hostId, listingId, connectionId).delete(),
|
|
521
|
+
connectionRecordRef(firestore(), actor.hostId, listingId, connectionId).delete()
|
|
522
|
+
]);
|
|
523
|
+
// The site's pages stop showing it now, not when their cache expires (AGL-4195).
|
|
524
|
+
await announceConnectionRecordChange(actor.hostId, `plugin connection ${listingId}/${connectionId} disconnected`);
|
|
525
|
+
await logHostActivity(actor.hostId, {
|
|
526
|
+
uid: actor.uid
|
|
527
|
+
}, `Disconnected a plugin connection (${record.provider})`, {
|
|
528
|
+
type: 'marketplace:plugin-connection',
|
|
529
|
+
id: `${listingId}/${connectionId}`
|
|
530
|
+
});
|
|
531
|
+
return routeJson({
|
|
532
|
+
connection: null,
|
|
533
|
+
revoked
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
export async function statusRoute(request) {
|
|
537
|
+
if (request.method !== 'GET') return methodNotAllowed('GET');
|
|
538
|
+
const params = await readParams(request);
|
|
539
|
+
const actor = await siteConnectionsGate(request, params, {
|
|
540
|
+
requirePlan: false
|
|
541
|
+
});
|
|
542
|
+
if (actor instanceof Response) return actor;
|
|
543
|
+
const onlyListing = isListingId(params['listingId']) ? params['listingId'] : null;
|
|
544
|
+
const [installed, records] = await Promise.all([
|
|
545
|
+
installedConnectionsForHost(firestore(), {
|
|
546
|
+
hostId: actor.hostId,
|
|
547
|
+
orgId: actor.orgId
|
|
548
|
+
}),
|
|
549
|
+
listHostConnections(firestore(), actor.hostId)
|
|
550
|
+
]);
|
|
551
|
+
const entries = [];
|
|
552
|
+
for (const plugin of installed){
|
|
553
|
+
if (onlyListing && plugin.listingId !== onlyListing) continue;
|
|
554
|
+
const set = await credentialsStatus(firestore(), plugin.listingId, plugin.connections.map((connection)=>connection.id));
|
|
555
|
+
for (const connection of plugin.connections){
|
|
556
|
+
var _connection_resources, _ref;
|
|
557
|
+
var _set_connection_id;
|
|
558
|
+
const record = records.find((one)=>one.listingId === plugin.listingId && one.connectionId === connection.id);
|
|
559
|
+
entries.push(_extends({
|
|
560
|
+
listingId: plugin.listingId,
|
|
561
|
+
connectionId: connection.id,
|
|
562
|
+
provider: connection.provider,
|
|
563
|
+
label: connection.label
|
|
564
|
+
}, connection.description ? {
|
|
565
|
+
description: connection.description
|
|
566
|
+
} : {}, {
|
|
567
|
+
resources: ((_connection_resources = connection.resources) != null ? _connection_resources : []).map((resource)=>({
|
|
568
|
+
id: resource.id,
|
|
569
|
+
label: resource.label
|
|
570
|
+
})),
|
|
571
|
+
connection: record ? toConnectionView(record, connection) : null,
|
|
572
|
+
credentialsSet: (_ref = (_set_connection_id = set[connection.id]) == null ? void 0 : _set_connection_id.set) != null ? _ref : false,
|
|
573
|
+
planAllows: actor.planAllows,
|
|
574
|
+
hosts: pluginConnectionHosts(connection)
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return routeJson({
|
|
579
|
+
configured: Boolean(readPluginConnectionsKeyring()),
|
|
580
|
+
planAllows: actor.planAllows,
|
|
581
|
+
connections: entries
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/* ------------------------------------------------------------------ */ /* Sync now */ /* ------------------------------------------------------------------ */ /** A second "Sync now" within this of the last sync that ran is answered with the connection as it is. */ export const PLUGIN_CONNECTION_SYNC_NOW_COOLDOWN_MS = 60000;
|
|
585
|
+
/** How long a "Sync now" may keep starting pages: the console's request has to answer. */ const SYNC_NOW_BUDGET_MS = 45000;
|
|
586
|
+
/**
|
|
587
|
+
* POST `{ hostId, listingId, connectionId }` (AGL-4194): runs the
|
|
588
|
+
* connection's declared sync once, now, as the scheduled sync would, and
|
|
589
|
+
* answers `{ connection, synced }` — the view after the run, which carries a
|
|
590
|
+
* failure as `status` and `lastError`, so a provider error is a 200 with
|
|
591
|
+
* `synced: false`, not a 502. Site key and plan.
|
|
592
|
+
*
|
|
593
|
+
* 409 `not-connected`, `reconnect`, `choose-first` (a resource not chosen)
|
|
594
|
+
* or `no-sync` (the pinned version declares no sync)
|
|
595
|
+
* 429 `too-soon` within a minute of the last sync, with the view
|
|
596
|
+
*/ export async function syncNowRoute(request) {
|
|
597
|
+
var _connection_resources;
|
|
598
|
+
const context = await connectedRoute(request, 'POST');
|
|
599
|
+
if (context instanceof Response) return context;
|
|
600
|
+
const { actor, connection, record, listingId, keyring, client, version } = context;
|
|
601
|
+
if (!connection.sync) {
|
|
602
|
+
return routeError(409, `${connection.label} declares nothing to sync.`, {
|
|
603
|
+
reason: 'no-sync'
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const missing = ((_connection_resources = connection.resources) != null ? _connection_resources : []).find((resource)=>!record.choices[resource.id]);
|
|
607
|
+
if (missing) {
|
|
608
|
+
return routeError(409, `Choose the ${missing.label.toLowerCase()} first.`, {
|
|
609
|
+
reason: 'choose-first',
|
|
610
|
+
resource: missing.id
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const nowMs = pluginConnectionRuntime.now();
|
|
614
|
+
if (record.lastSyncAtMs && nowMs - record.lastSyncAtMs < PLUGIN_CONNECTION_SYNC_NOW_COOLDOWN_MS) {
|
|
615
|
+
return routeError(429, `${connection.label} synced less than a minute ago.`, {
|
|
616
|
+
reason: 'too-soon',
|
|
617
|
+
retryAfterMs: PLUGIN_CONNECTION_SYNC_NOW_COOLDOWN_MS - (nowMs - record.lastSyncAtMs),
|
|
618
|
+
connection: toConnectionView(record, connection)
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const outcome = await runConnectionSync({
|
|
622
|
+
keyring,
|
|
623
|
+
record,
|
|
624
|
+
connection,
|
|
625
|
+
version,
|
|
626
|
+
client,
|
|
627
|
+
deadlineMs: nowMs + SYNC_NOW_BUDGET_MS
|
|
628
|
+
});
|
|
629
|
+
const next = await getConnection(firestore(), actor.hostId, listingId, connection.id);
|
|
630
|
+
return routeJson({
|
|
631
|
+
connection: next ? toConnectionView(next, connection) : null,
|
|
632
|
+
synced: outcome === 'synced'
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
/* ------------------------------------------------------------------ */ /* The publisher's OAuth app */ /* ------------------------------------------------------------------ */ /**
|
|
636
|
+
* GET: whether each connection the listing's latest version declares has its
|
|
637
|
+
* OAuth app set. POST `{ listingId, connectionId, clientId, clientSecret }`
|
|
638
|
+
* sets or replaces the pair; `{ listingId, connectionId, clear: true }`
|
|
639
|
+
* removes it. Never answers either value. Asks "Set a listing's connection
|
|
640
|
+
* credentials" of a member of the workspace that published the listing.
|
|
641
|
+
*/ export async function credentialsRoute(request) {
|
|
642
|
+
var _listing_get;
|
|
643
|
+
if (request.method !== 'GET' && request.method !== 'POST') return methodNotAllowed('GET, POST');
|
|
644
|
+
const params = await readParams(request);
|
|
645
|
+
const caller = await signedIn(request);
|
|
646
|
+
if (caller instanceof Response) return caller;
|
|
647
|
+
const listingId = params['listingId'];
|
|
648
|
+
if (!isListingId(listingId)) return routeError(400, 'Missing listingId');
|
|
649
|
+
const listing = await firestore().collection('marketplaceListings').doc(listingId).get();
|
|
650
|
+
if (!listing.exists || listing.get('deletedAt')) return routeError(404, 'Unknown listing');
|
|
651
|
+
const publisherOrgId = listing.get('profileId');
|
|
652
|
+
// The key spelled out (`PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION`), so the
|
|
653
|
+
// repo's enforcement guard finds the door that asks it.
|
|
654
|
+
const verdict = await publisherPermissionVerdict(caller.uid, typeof publisherOrgId === 'string' ? publisherOrgId : null, 'marketplace.connectionCredentials');
|
|
655
|
+
if (verdict === 'not-a-member') return routeError(403, 'Not your listing');
|
|
656
|
+
if (verdict === 'refused') {
|
|
657
|
+
return routeError(403, pluginConnectionsPermissionRefusal(PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION), {
|
|
658
|
+
reason: 'permission',
|
|
659
|
+
permission: PLUGIN_CONNECTIONS_CREDENTIALS_PERMISSION
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
const declared = await latestListingConnections(firestore(), listingId, String((_listing_get = listing.get('latestVersion')) != null ? _listing_get : ''));
|
|
663
|
+
const answer = async ()=>{
|
|
664
|
+
const set = await credentialsStatus(firestore(), listingId, declared.map((connection)=>connection.id));
|
|
665
|
+
const credentials = declared.map((connection)=>{
|
|
666
|
+
var _ref, _ref1;
|
|
667
|
+
var _set_connection_id, _set_connection_id1;
|
|
668
|
+
return {
|
|
669
|
+
connectionId: connection.id,
|
|
670
|
+
label: connection.label,
|
|
671
|
+
set: (_ref = (_set_connection_id = set[connection.id]) == null ? void 0 : _set_connection_id.set) != null ? _ref : false,
|
|
672
|
+
updatedAtMs: (_ref1 = (_set_connection_id1 = set[connection.id]) == null ? void 0 : _set_connection_id1.updatedAtMs) != null ? _ref1 : null
|
|
673
|
+
};
|
|
674
|
+
});
|
|
675
|
+
return routeJson({
|
|
676
|
+
configured: Boolean(readPluginConnectionsKeyring()),
|
|
677
|
+
credentials
|
|
678
|
+
});
|
|
679
|
+
};
|
|
680
|
+
if (request.method === 'GET') return answer();
|
|
681
|
+
const connection = declared.find((one)=>one.id === params['connectionId']);
|
|
682
|
+
if (!connection) return routeError(404, 'The listing’s latest version declares no such connection.');
|
|
683
|
+
if (params['clear'] === true) {
|
|
684
|
+
await clearCredentials(firestore(), listingId, connection.id);
|
|
685
|
+
return answer();
|
|
686
|
+
}
|
|
687
|
+
const clientId = typeof params['clientId'] === 'string' ? params['clientId'].trim() : '';
|
|
688
|
+
const clientSecret = typeof params['clientSecret'] === 'string' ? params['clientSecret'].trim() : '';
|
|
689
|
+
if (!CLIENT_ID_PATTERN.test(clientId) || !CLIENT_SECRET_PATTERN.test(clientSecret)) {
|
|
690
|
+
return routeError(400, 'Enter the OAuth app’s client id and client secret.');
|
|
691
|
+
}
|
|
692
|
+
const keyring = keyringOr503();
|
|
693
|
+
if (keyring instanceof Response) return keyring;
|
|
694
|
+
await saveCredentials(firestore(), sealCredentials(keyring, {
|
|
695
|
+
orgId: String(publisherOrgId),
|
|
696
|
+
listingId,
|
|
697
|
+
connectionId: connection.id,
|
|
698
|
+
clientId,
|
|
699
|
+
clientSecret,
|
|
700
|
+
uid: caller.uid,
|
|
701
|
+
nowMs: pluginConnectionRuntime.now()
|
|
702
|
+
}));
|
|
703
|
+
return answer();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
//# sourceMappingURL=routes.js.map
|