@voyant-travel/apps 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-runtime.d.ts +3 -0
- package/dist/app-api-routes.js +1 -1
- package/dist/app-api-service.js +1 -1
- package/dist/contracts.d.ts +71 -0
- package/dist/contracts.js +66 -0
- package/dist/extension-resolution.d.ts +72 -0
- package/dist/extension-resolution.js +159 -0
- package/dist/extension-resolution.test.d.ts +1 -0
- package/dist/extension-resolution.test.js +111 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/installation-read-model.d.ts +64 -0
- package/dist/installation-read-model.js +148 -0
- package/dist/installation-routes.test.d.ts +1 -0
- package/dist/installation-routes.test.js +161 -0
- package/dist/locale-resolution.d.ts +59 -0
- package/dist/locale-resolution.js +113 -0
- package/dist/locale-resolution.test.d.ts +1 -0
- package/dist/locale-resolution.test.js +48 -0
- package/dist/routes.d.ts +25 -0
- package/dist/routes.js +135 -2
- package/dist/schema.d.ts +237 -0
- package/dist/schema.js +27 -0
- package/dist/session-token-service.d.ts +47 -0
- package/dist/session-token-service.js +127 -0
- package/dist/session-token.d.ts +82 -0
- package/dist/session-token.js +0 -0
- package/dist/session-token.test.d.ts +1 -0
- package/dist/session-token.test.js +97 -0
- package/dist/voyant.js +52 -2
- package/migrations/20260717150000_app_session_tokens.sql +18 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +23 -3
package/dist/api-runtime.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export declare const createAppsApiModule: import("@voyant-travel/core/project").
|
|
|
3
3
|
name: string;
|
|
4
4
|
};
|
|
5
5
|
adminRoutes: import("hono").Hono<{
|
|
6
|
+
Bindings: {
|
|
7
|
+
VOYANT_CLOUD_DEPLOYMENT_ID?: string;
|
|
8
|
+
};
|
|
6
9
|
Variables: {
|
|
7
10
|
db: import("drizzle-orm/postgres-js").PostgresJsDatabase;
|
|
8
11
|
};
|
package/dist/app-api-routes.js
CHANGED
|
@@ -72,7 +72,7 @@ export function createAppsAppApiRoutes(options = {}) {
|
|
|
72
72
|
});
|
|
73
73
|
routes.get("/webhooks", (c) => run(c, service.listWebhookHealth(c.get("db"), appContext(c)), options.deadlineMs));
|
|
74
74
|
routes.post("/webhooks/replay", async (c) => {
|
|
75
|
-
await service.requireAccess(c.get("db"), appContext(c), ["webhooks:replay"]);
|
|
75
|
+
await service.requireAccess(c.get("db"), appContext(c), ["app-webhooks:replay"]);
|
|
76
76
|
const body = await parseJsonBody(c, appApiWebhookReplaySchema);
|
|
77
77
|
return run(c, replayAppWebhookDelivery(c.get("db"), {
|
|
78
78
|
deliveryId: body.deliveryId,
|
package/dist/app-api-service.js
CHANGED
|
@@ -99,7 +99,7 @@ export function createAppApiService(options = {}) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
async function listWebhookHealth(db, context) {
|
|
102
|
-
await requireAccess(db, context, ["webhooks:read"]);
|
|
102
|
+
await requireAccess(db, context, ["app-webhooks:read"]);
|
|
103
103
|
const data = await db
|
|
104
104
|
.select()
|
|
105
105
|
.from(appWebhookSubscriptions)
|
package/dist/contracts.d.ts
CHANGED
|
@@ -170,6 +170,44 @@ export declare const appListQuerySchema: z.ZodObject<{
|
|
|
170
170
|
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
171
171
|
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
172
172
|
}, z.core.$strip>;
|
|
173
|
+
export declare const appInstallationListQuerySchema: z.ZodObject<{
|
|
174
|
+
appId: z.ZodOptional<z.ZodString>;
|
|
175
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
176
|
+
active: "active";
|
|
177
|
+
pending: "pending";
|
|
178
|
+
authorizing: "authorizing";
|
|
179
|
+
paused: "paused";
|
|
180
|
+
degraded: "degraded";
|
|
181
|
+
revoked: "revoked";
|
|
182
|
+
uninstalled: "uninstalled";
|
|
183
|
+
}>>;
|
|
184
|
+
deploymentId: z.ZodOptional<z.ZodString>;
|
|
185
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
186
|
+
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
187
|
+
}, z.core.$strip>;
|
|
188
|
+
export declare const appInstallationAuditQuerySchema: z.ZodObject<{
|
|
189
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
190
|
+
}, z.core.$strip>;
|
|
191
|
+
export declare const installAppSchema: z.ZodObject<{
|
|
192
|
+
appId: z.ZodString;
|
|
193
|
+
releaseId: z.ZodString;
|
|
194
|
+
actorId: z.ZodString;
|
|
195
|
+
grantedOptionalScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
196
|
+
updatePolicy: z.ZodOptional<z.ZodEnum<{
|
|
197
|
+
manual: "manual";
|
|
198
|
+
compatible: "compatible";
|
|
199
|
+
patch: "patch";
|
|
200
|
+
pinned: "pinned";
|
|
201
|
+
}>>;
|
|
202
|
+
deploymentId: z.ZodOptional<z.ZodString>;
|
|
203
|
+
}, z.core.$strict>;
|
|
204
|
+
export declare const lifecycleActionBodySchema: z.ZodObject<{
|
|
205
|
+
actorId: z.ZodString;
|
|
206
|
+
}, z.core.$strict>;
|
|
207
|
+
export declare const activateInstallationBodySchema: z.ZodObject<{
|
|
208
|
+
releaseId: z.ZodString;
|
|
209
|
+
actorId: z.ZodString;
|
|
210
|
+
}, z.core.$strict>;
|
|
173
211
|
export declare const appWebhookReplaySchema: z.ZodObject<{
|
|
174
212
|
deliveryId: z.ZodString;
|
|
175
213
|
actorId: z.ZodString;
|
|
@@ -253,11 +291,44 @@ export declare const appCredentialRevocationSchema: z.ZodObject<{
|
|
|
253
291
|
installationId: z.ZodString;
|
|
254
292
|
actorId: z.ZodString;
|
|
255
293
|
}, z.core.$strict>;
|
|
294
|
+
export declare const appSessionTokenIssueSchema: z.ZodObject<{
|
|
295
|
+
entity: z.ZodOptional<z.ZodObject<{
|
|
296
|
+
type: z.ZodString;
|
|
297
|
+
id: z.ZodString;
|
|
298
|
+
}, z.core.$strict>>;
|
|
299
|
+
slot: z.ZodOptional<z.ZodString>;
|
|
300
|
+
}, z.core.$strict>;
|
|
301
|
+
export declare const appSessionTokenExchangeSchema: z.ZodPipe<z.ZodObject<{
|
|
302
|
+
session_token: z.ZodString;
|
|
303
|
+
client_id: z.ZodString;
|
|
304
|
+
client_secret: z.ZodOptional<z.ZodString>;
|
|
305
|
+
viewer_scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
306
|
+
contextual_scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
307
|
+
}, z.core.$strict>, z.ZodTransform<{
|
|
308
|
+
client_secret: string | undefined;
|
|
309
|
+
session_token: string;
|
|
310
|
+
client_id: string;
|
|
311
|
+
viewer_scopes: string[];
|
|
312
|
+
contextual_scopes?: string[] | undefined;
|
|
313
|
+
}, {
|
|
314
|
+
session_token: string;
|
|
315
|
+
client_id: string;
|
|
316
|
+
viewer_scopes: string[];
|
|
317
|
+
client_secret?: string | undefined;
|
|
318
|
+
contextual_scopes?: string[] | undefined;
|
|
319
|
+
}>>;
|
|
256
320
|
export type AppManifest = z.infer<typeof appManifestSchema>;
|
|
257
321
|
export type CreateCustomAppRegistrationInput = z.infer<typeof createCustomAppRegistrationSchema>;
|
|
258
322
|
export type ReleaseManifestUploadInput = z.infer<typeof releaseManifestUploadSchema>;
|
|
259
323
|
export type ReleaseManifestFetchInput = z.infer<typeof releaseManifestFetchSchema>;
|
|
260
324
|
export type AppListQuery = z.infer<typeof appListQuerySchema>;
|
|
325
|
+
export type AppInstallationListQuery = z.infer<typeof appInstallationListQuerySchema>;
|
|
326
|
+
export type AppInstallationAuditQuery = z.infer<typeof appInstallationAuditQuerySchema>;
|
|
327
|
+
export type InstallAppRequest = z.infer<typeof installAppSchema>;
|
|
328
|
+
export type LifecycleActionBody = z.infer<typeof lifecycleActionBodySchema>;
|
|
329
|
+
export type ActivateInstallationBody = z.infer<typeof activateInstallationBodySchema>;
|
|
261
330
|
export type AppWebhookReplayInput = z.infer<typeof appWebhookReplaySchema>;
|
|
262
331
|
export type AppOAuthAuthorizeQuery = z.infer<typeof appOAuthAuthorizeQuerySchema>;
|
|
263
332
|
export type AppOAuthTokenInput = z.infer<typeof appOAuthTokenSchema>;
|
|
333
|
+
export type AppSessionTokenIssueInput = z.infer<typeof appSessionTokenIssueSchema>;
|
|
334
|
+
export type AppSessionTokenExchangeInput = z.infer<typeof appSessionTokenExchangeSchema>;
|
package/dist/contracts.js
CHANGED
|
@@ -213,6 +213,47 @@ export const appListQuerySchema = z.object({
|
|
|
213
213
|
limit: z.coerce.number().int().positive().max(100).default(25),
|
|
214
214
|
offset: z.coerce.number().int().nonnegative().default(0),
|
|
215
215
|
});
|
|
216
|
+
const appInstallationStatusValues = [
|
|
217
|
+
"pending",
|
|
218
|
+
"authorizing",
|
|
219
|
+
"active",
|
|
220
|
+
"paused",
|
|
221
|
+
"degraded",
|
|
222
|
+
"revoked",
|
|
223
|
+
"uninstalled",
|
|
224
|
+
];
|
|
225
|
+
const appInstallationUpdatePolicyValues = ["manual", "compatible", "patch", "pinned"];
|
|
226
|
+
export const appInstallationListQuerySchema = z.object({
|
|
227
|
+
appId: z.string().trim().min(1).optional(),
|
|
228
|
+
status: z.enum(appInstallationStatusValues).optional(),
|
|
229
|
+
deploymentId: z.string().trim().min(1).optional(),
|
|
230
|
+
limit: z.coerce.number().int().positive().max(100).default(25),
|
|
231
|
+
offset: z.coerce.number().int().nonnegative().default(0),
|
|
232
|
+
});
|
|
233
|
+
export const appInstallationAuditQuerySchema = z.object({
|
|
234
|
+
limit: z.coerce.number().int().positive().max(200).default(50),
|
|
235
|
+
});
|
|
236
|
+
export const installAppSchema = z
|
|
237
|
+
.object({
|
|
238
|
+
appId: z.string().trim().min(1),
|
|
239
|
+
releaseId: z.string().trim().min(1),
|
|
240
|
+
actorId: z.string().trim().min(1).max(160),
|
|
241
|
+
grantedOptionalScopes: z.array(scopeSchema).optional(),
|
|
242
|
+
updatePolicy: z.enum(appInstallationUpdatePolicyValues).optional(),
|
|
243
|
+
deploymentId: z.string().trim().min(1).optional(),
|
|
244
|
+
})
|
|
245
|
+
.strict();
|
|
246
|
+
export const lifecycleActionBodySchema = z
|
|
247
|
+
.object({
|
|
248
|
+
actorId: z.string().trim().min(1).max(160),
|
|
249
|
+
})
|
|
250
|
+
.strict();
|
|
251
|
+
export const activateInstallationBodySchema = z
|
|
252
|
+
.object({
|
|
253
|
+
releaseId: z.string().trim().min(1),
|
|
254
|
+
actorId: z.string().trim().min(1).max(160),
|
|
255
|
+
})
|
|
256
|
+
.strict();
|
|
216
257
|
export const appWebhookReplaySchema = z
|
|
217
258
|
.object({
|
|
218
259
|
deliveryId: z.string().trim().min(1),
|
|
@@ -271,3 +312,28 @@ export const appCredentialRevocationSchema = z
|
|
|
271
312
|
actorId: z.string().trim().min(1),
|
|
272
313
|
})
|
|
273
314
|
.strict();
|
|
315
|
+
const sessionTokenEntitySchema = z
|
|
316
|
+
.object({
|
|
317
|
+
type: z.string().trim().min(1).max(80),
|
|
318
|
+
id: z.string().trim().min(1).max(200),
|
|
319
|
+
})
|
|
320
|
+
.strict();
|
|
321
|
+
export const appSessionTokenIssueSchema = z
|
|
322
|
+
.object({
|
|
323
|
+
entity: sessionTokenEntitySchema.optional(),
|
|
324
|
+
slot: z.string().trim().min(1).max(80).optional(),
|
|
325
|
+
})
|
|
326
|
+
.strict();
|
|
327
|
+
export const appSessionTokenExchangeSchema = z
|
|
328
|
+
.object({
|
|
329
|
+
session_token: z.string().trim().min(1),
|
|
330
|
+
client_id: z.string().trim().min(1),
|
|
331
|
+
client_secret: z.string().trim().optional(),
|
|
332
|
+
viewer_scopes: z.array(scopeSchema).default([]),
|
|
333
|
+
contextual_scopes: z.array(scopeSchema).optional(),
|
|
334
|
+
})
|
|
335
|
+
.strict()
|
|
336
|
+
.transform((input) => ({
|
|
337
|
+
...input,
|
|
338
|
+
client_secret: input.client_secret?.trim() || undefined,
|
|
339
|
+
}));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installation-backed admin extension resolution (RFC §6, Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the static self-hosted descriptor list as the source of activated
|
|
5
|
+
* admin extensions: descriptors come from `app_extension_installations` for
|
|
6
|
+
* `active` installations only, so pausing, degrading, or uninstalling an app
|
|
7
|
+
* immediately drops its pages and slots. Slot extensions are filtered by
|
|
8
|
+
* release-pinned `extensionApi` compatibility, and host-rendered labels (nav
|
|
9
|
+
* entries, extension titles) plus the resolved app locale + text direction come
|
|
10
|
+
* from the installed release's declared locales and `app_release_localizations`.
|
|
11
|
+
*
|
|
12
|
+
* The returned descriptors are the exact shape the admin host consumes; static
|
|
13
|
+
* deployment-graph descriptors remain supported alongside these.
|
|
14
|
+
*/
|
|
15
|
+
import { type UiExtensionDescriptor } from "@voyant-travel/admin-extension-sdk";
|
|
16
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
17
|
+
import { type AppTextDirection, type HostLabelRow } from "./locale-resolution.js";
|
|
18
|
+
/** A resolved slot/dashboard extension plus the app locale its frame renders in. */
|
|
19
|
+
export interface ResolvedUiSlotExtension {
|
|
20
|
+
descriptor: UiExtensionDescriptor;
|
|
21
|
+
installationId: string;
|
|
22
|
+
appId: string;
|
|
23
|
+
appLocale: string;
|
|
24
|
+
direction: AppTextDirection;
|
|
25
|
+
}
|
|
26
|
+
/** A resolved full-page app extension mounted under the app-owned admin route. */
|
|
27
|
+
export interface ResolvedAppPage {
|
|
28
|
+
/** Stable per-installation key (`<installationId>:<manifest key>`). */
|
|
29
|
+
key: string;
|
|
30
|
+
installationId: string;
|
|
31
|
+
appId: string;
|
|
32
|
+
/** App-owned admin sub-path declared by the manifest (e.g. `/settings`). */
|
|
33
|
+
path: string;
|
|
34
|
+
entryUrl: string;
|
|
35
|
+
/** Host-rendered page title, locale-resolved with deterministic fallback. */
|
|
36
|
+
title: string;
|
|
37
|
+
/** Host-rendered navigation label for the page's nav entry. */
|
|
38
|
+
navLabel: string;
|
|
39
|
+
appLocale: string;
|
|
40
|
+
direction: AppTextDirection;
|
|
41
|
+
}
|
|
42
|
+
export interface ResolvedInstalledExtensions {
|
|
43
|
+
slots: ResolvedUiSlotExtension[];
|
|
44
|
+
pages: ResolvedAppPage[];
|
|
45
|
+
}
|
|
46
|
+
export interface ResolveInstalledExtensionsInput {
|
|
47
|
+
deploymentId: string;
|
|
48
|
+
/** The staff member's active admin locale. */
|
|
49
|
+
activeLocale: string;
|
|
50
|
+
/** The extension API version the host implements (defaults to the SDK's). */
|
|
51
|
+
extensionApiVersion?: string;
|
|
52
|
+
}
|
|
53
|
+
/** One row of an active installation's active extension, as read from the store. */
|
|
54
|
+
export interface InstalledExtensionRow {
|
|
55
|
+
installationId: string;
|
|
56
|
+
appId: string;
|
|
57
|
+
extensionKey: string;
|
|
58
|
+
descriptor: Record<string, unknown>;
|
|
59
|
+
releaseId: string;
|
|
60
|
+
defaultLocale: string;
|
|
61
|
+
supportedLocales: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
export declare function resolveInstalledExtensions(db: PostgresJsDatabase, input: ResolveInstalledExtensionsInput): Promise<ResolvedInstalledExtensions>;
|
|
64
|
+
/**
|
|
65
|
+
* Pure assembly of resolved descriptors from store rows and localization data.
|
|
66
|
+
* Applies compatibility filtering, locale resolution, and host-label lookup.
|
|
67
|
+
* The `rows` are assumed to already be scoped to active installations/extensions.
|
|
68
|
+
*/
|
|
69
|
+
export declare function assembleInstalledExtensions(rows: readonly InstalledExtensionRow[], localizationsByRelease: Map<string, HostLabelRow[]>, options: {
|
|
70
|
+
activeLocale: string;
|
|
71
|
+
extensionApiVersion?: string;
|
|
72
|
+
}): ResolvedInstalledExtensions;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installation-backed admin extension resolution (RFC §6, Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the static self-hosted descriptor list as the source of activated
|
|
5
|
+
* admin extensions: descriptors come from `app_extension_installations` for
|
|
6
|
+
* `active` installations only, so pausing, degrading, or uninstalling an app
|
|
7
|
+
* immediately drops its pages and slots. Slot extensions are filtered by
|
|
8
|
+
* release-pinned `extensionApi` compatibility, and host-rendered labels (nav
|
|
9
|
+
* entries, extension titles) plus the resolved app locale + text direction come
|
|
10
|
+
* from the installed release's declared locales and `app_release_localizations`.
|
|
11
|
+
*
|
|
12
|
+
* The returned descriptors are the exact shape the admin host consumes; static
|
|
13
|
+
* deployment-graph descriptors remain supported alongside these.
|
|
14
|
+
*/
|
|
15
|
+
import { ADMIN_UI_EXTENSION_API_VERSION, isUiExtensionCompatible, } from "@voyant-travel/admin-extension-sdk";
|
|
16
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
17
|
+
import { createHostLabelResolver, resolveAppLocale, } from "./locale-resolution.js";
|
|
18
|
+
import { appExtensionInstallations, appInstallations, appReleaseLocalizations, appReleases, } from "./schema.js";
|
|
19
|
+
export async function resolveInstalledExtensions(db, input) {
|
|
20
|
+
const rows = await db
|
|
21
|
+
.select({
|
|
22
|
+
installationId: appInstallations.id,
|
|
23
|
+
appId: appInstallations.appId,
|
|
24
|
+
extensionKey: appExtensionInstallations.extensionKey,
|
|
25
|
+
descriptor: appExtensionInstallations.descriptor,
|
|
26
|
+
releaseId: appExtensionInstallations.releaseId,
|
|
27
|
+
defaultLocale: appReleases.defaultLocale,
|
|
28
|
+
supportedLocales: appReleases.supportedLocales,
|
|
29
|
+
})
|
|
30
|
+
.from(appExtensionInstallations)
|
|
31
|
+
.innerJoin(appInstallations, eq(appExtensionInstallations.installationId, appInstallations.id))
|
|
32
|
+
.innerJoin(appReleases, eq(appExtensionInstallations.releaseId, appReleases.id))
|
|
33
|
+
.where(and(eq(appInstallations.deploymentId, input.deploymentId),
|
|
34
|
+
// Active installations only: paused/degraded/uninstalled apps unmount.
|
|
35
|
+
eq(appInstallations.status, "active"), eq(appExtensionInstallations.status, "active")));
|
|
36
|
+
const localizationsByRelease = await loadLocalizations(db, rows.map((row) => row.releaseId));
|
|
37
|
+
return assembleInstalledExtensions(rows, localizationsByRelease, {
|
|
38
|
+
activeLocale: input.activeLocale,
|
|
39
|
+
extensionApiVersion: input.extensionApiVersion,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pure assembly of resolved descriptors from store rows and localization data.
|
|
44
|
+
* Applies compatibility filtering, locale resolution, and host-label lookup.
|
|
45
|
+
* The `rows` are assumed to already be scoped to active installations/extensions.
|
|
46
|
+
*/
|
|
47
|
+
export function assembleInstalledExtensions(rows, localizationsByRelease, options) {
|
|
48
|
+
const version = options.extensionApiVersion ?? ADMIN_UI_EXTENSION_API_VERSION;
|
|
49
|
+
const slots = [];
|
|
50
|
+
const pages = [];
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const declaration = {
|
|
53
|
+
defaultLocale: row.defaultLocale,
|
|
54
|
+
supportedLocales: row.supportedLocales,
|
|
55
|
+
};
|
|
56
|
+
const locale = resolveAppLocale(options.activeLocale, declaration);
|
|
57
|
+
const labels = createHostLabelResolver(localizationsByRelease.get(row.releaseId) ?? [], locale.appLocale, declaration.defaultLocale);
|
|
58
|
+
if (row.extensionKey.startsWith("page:")) {
|
|
59
|
+
const page = parsePageDescriptor(row.descriptor);
|
|
60
|
+
if (!page)
|
|
61
|
+
continue;
|
|
62
|
+
const title = labels.resolve(page.titleKey, ["extension", "navigation"]) ?? page.key;
|
|
63
|
+
const navLabel = labels.resolve(page.titleKey, ["navigation", "extension"]) ?? title;
|
|
64
|
+
pages.push({
|
|
65
|
+
key: `${row.installationId}:${page.key}`,
|
|
66
|
+
installationId: row.installationId,
|
|
67
|
+
appId: row.appId,
|
|
68
|
+
path: page.path,
|
|
69
|
+
entryUrl: page.entryUrl,
|
|
70
|
+
title,
|
|
71
|
+
navLabel,
|
|
72
|
+
appLocale: locale.appLocale,
|
|
73
|
+
direction: locale.direction,
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (row.extensionKey.startsWith("slot:")) {
|
|
78
|
+
const slot = parseSlotDescriptor(row.descriptor);
|
|
79
|
+
if (!slot)
|
|
80
|
+
continue;
|
|
81
|
+
// Release-pinned compatibility filter: an incompatible extension is not
|
|
82
|
+
// mounted (the host's render-time check is defense-in-depth).
|
|
83
|
+
if (!isUiExtensionCompatible(slot.extensionApi, version))
|
|
84
|
+
continue;
|
|
85
|
+
const displayName = labels.resolve(slot.titleKey, ["extension", "navigation"]) ?? slot.key;
|
|
86
|
+
slots.push({
|
|
87
|
+
descriptor: {
|
|
88
|
+
key: `${row.installationId}:${slot.key}`,
|
|
89
|
+
version: slot.version,
|
|
90
|
+
displayName,
|
|
91
|
+
extensionApi: slot.extensionApi,
|
|
92
|
+
entryUrl: slot.entryUrl,
|
|
93
|
+
slots: slot.slots,
|
|
94
|
+
...(slot.config ? { config: slot.config } : {}),
|
|
95
|
+
},
|
|
96
|
+
installationId: row.installationId,
|
|
97
|
+
appId: row.appId,
|
|
98
|
+
appLocale: locale.appLocale,
|
|
99
|
+
direction: locale.direction,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { slots, pages };
|
|
104
|
+
}
|
|
105
|
+
async function loadLocalizations(db, releaseIds) {
|
|
106
|
+
const unique = [...new Set(releaseIds)];
|
|
107
|
+
const grouped = new Map();
|
|
108
|
+
if (unique.length === 0)
|
|
109
|
+
return grouped;
|
|
110
|
+
const rows = await db
|
|
111
|
+
.select({
|
|
112
|
+
releaseId: appReleaseLocalizations.releaseId,
|
|
113
|
+
locale: appReleaseLocalizations.locale,
|
|
114
|
+
surface: appReleaseLocalizations.surface,
|
|
115
|
+
messageKey: appReleaseLocalizations.messageKey,
|
|
116
|
+
text: appReleaseLocalizations.text,
|
|
117
|
+
})
|
|
118
|
+
.from(appReleaseLocalizations)
|
|
119
|
+
.where(inArray(appReleaseLocalizations.releaseId, unique));
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
const list = grouped.get(row.releaseId) ?? [];
|
|
122
|
+
list.push({
|
|
123
|
+
locale: row.locale,
|
|
124
|
+
surface: row.surface,
|
|
125
|
+
messageKey: row.messageKey,
|
|
126
|
+
text: row.text,
|
|
127
|
+
});
|
|
128
|
+
grouped.set(row.releaseId, list);
|
|
129
|
+
}
|
|
130
|
+
return grouped;
|
|
131
|
+
}
|
|
132
|
+
function parsePageDescriptor(value) {
|
|
133
|
+
const key = stringField(value.key);
|
|
134
|
+
const titleKey = stringField(value.titleKey);
|
|
135
|
+
const path = stringField(value.path);
|
|
136
|
+
const entryUrl = stringField(value.entryUrl);
|
|
137
|
+
if (!key || !titleKey || !path || !entryUrl)
|
|
138
|
+
return null;
|
|
139
|
+
return { key, titleKey, path, entryUrl };
|
|
140
|
+
}
|
|
141
|
+
function parseSlotDescriptor(value) {
|
|
142
|
+
const key = stringField(value.key);
|
|
143
|
+
const titleKey = stringField(value.titleKey);
|
|
144
|
+
const version = stringField(value.version);
|
|
145
|
+
const extensionApi = stringField(value.extensionApi);
|
|
146
|
+
const entryUrl = stringField(value.entryUrl);
|
|
147
|
+
const slots = Array.isArray(value.slots)
|
|
148
|
+
? value.slots.filter((slot) => typeof slot === "string")
|
|
149
|
+
: [];
|
|
150
|
+
if (!key || !titleKey || !version || !extensionApi || !entryUrl || slots.length === 0)
|
|
151
|
+
return null;
|
|
152
|
+
const config = value.config && typeof value.config === "object" && !Array.isArray(value.config)
|
|
153
|
+
? value.config
|
|
154
|
+
: undefined;
|
|
155
|
+
return { key, titleKey, version, extensionApi, entryUrl, slots, config };
|
|
156
|
+
}
|
|
157
|
+
function stringField(value) {
|
|
158
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { assembleInstalledExtensions } from "./extension-resolution.js";
|
|
3
|
+
function slotRow(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
installationId: "apin_1",
|
|
6
|
+
appId: "app_1",
|
|
7
|
+
extensionKey: "slot:reports",
|
|
8
|
+
releaseId: "aprl_1",
|
|
9
|
+
defaultLocale: "en",
|
|
10
|
+
supportedLocales: ["en", "pt"],
|
|
11
|
+
descriptor: {
|
|
12
|
+
key: "reports",
|
|
13
|
+
titleKey: "reports.title",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
extensionApi: "^1",
|
|
16
|
+
entryUrl: "https://app.example.com/reports",
|
|
17
|
+
slots: ["dashboard.header"],
|
|
18
|
+
},
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function pageRow(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
installationId: "apin_1",
|
|
25
|
+
appId: "app_1",
|
|
26
|
+
extensionKey: "page:settings",
|
|
27
|
+
releaseId: "aprl_1",
|
|
28
|
+
defaultLocale: "en",
|
|
29
|
+
supportedLocales: ["en", "pt"],
|
|
30
|
+
descriptor: {
|
|
31
|
+
key: "settings",
|
|
32
|
+
titleKey: "settings.title",
|
|
33
|
+
path: "/settings",
|
|
34
|
+
entryUrl: "https://app.example.com/settings",
|
|
35
|
+
},
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const localizations = new Map([
|
|
40
|
+
[
|
|
41
|
+
"aprl_1",
|
|
42
|
+
[
|
|
43
|
+
{ locale: "en", surface: "extension", messageKey: "reports.title", text: "Reports" },
|
|
44
|
+
{ locale: "pt", surface: "extension", messageKey: "reports.title", text: "Relatórios" },
|
|
45
|
+
{ locale: "en", surface: "navigation", messageKey: "settings.title", text: "Settings" },
|
|
46
|
+
{ locale: "en", surface: "extension", messageKey: "settings.title", text: "App Settings" },
|
|
47
|
+
],
|
|
48
|
+
],
|
|
49
|
+
]);
|
|
50
|
+
describe("assembleInstalledExtensions", () => {
|
|
51
|
+
it("resolves slot descriptors with host labels and locale/direction", () => {
|
|
52
|
+
const result = assembleInstalledExtensions([slotRow()], localizations, { activeLocale: "pt" });
|
|
53
|
+
expect(result.slots).toHaveLength(1);
|
|
54
|
+
const slot = result.slots[0];
|
|
55
|
+
expect(slot?.descriptor.key).toBe("apin_1:reports");
|
|
56
|
+
expect(slot?.descriptor.displayName).toBe("Relatórios");
|
|
57
|
+
expect(slot?.descriptor.slots).toEqual(["dashboard.header"]);
|
|
58
|
+
expect(slot?.appLocale).toBe("pt");
|
|
59
|
+
expect(slot?.direction).toBe("ltr");
|
|
60
|
+
});
|
|
61
|
+
it("resolves full-page descriptors with title and nav label", () => {
|
|
62
|
+
const result = assembleInstalledExtensions([pageRow()], localizations, { activeLocale: "en" });
|
|
63
|
+
expect(result.pages).toHaveLength(1);
|
|
64
|
+
const page = result.pages[0];
|
|
65
|
+
expect(page?.key).toBe("apin_1:settings");
|
|
66
|
+
expect(page?.path).toBe("/settings");
|
|
67
|
+
expect(page?.title).toBe("App Settings");
|
|
68
|
+
expect(page?.navLabel).toBe("Settings");
|
|
69
|
+
});
|
|
70
|
+
it("filters out extensionApi-incompatible slot extensions", () => {
|
|
71
|
+
const incompatible = slotRow({
|
|
72
|
+
descriptor: {
|
|
73
|
+
key: "legacy",
|
|
74
|
+
titleKey: "reports.title",
|
|
75
|
+
version: "1.0.0",
|
|
76
|
+
extensionApi: "^0.9",
|
|
77
|
+
entryUrl: "https://app.example.com/legacy",
|
|
78
|
+
slots: ["dashboard.header"],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const result = assembleInstalledExtensions([incompatible], localizations, {
|
|
82
|
+
activeLocale: "en",
|
|
83
|
+
extensionApiVersion: "1.1.0",
|
|
84
|
+
});
|
|
85
|
+
expect(result.slots).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
it("falls back to the descriptor key when no host label exists", () => {
|
|
88
|
+
const result = assembleInstalledExtensions([slotRow({ releaseId: "aprl_none" })], new Map(), {
|
|
89
|
+
activeLocale: "en",
|
|
90
|
+
});
|
|
91
|
+
expect(result.slots[0]?.descriptor.displayName).toBe("reports");
|
|
92
|
+
});
|
|
93
|
+
it("falls back to the default locale for an unsupported active locale", () => {
|
|
94
|
+
const result = assembleInstalledExtensions([slotRow()], localizations, { activeLocale: "de" });
|
|
95
|
+
expect(result.slots[0]?.appLocale).toBe("en");
|
|
96
|
+
expect(result.slots[0]?.descriptor.displayName).toBe("Reports");
|
|
97
|
+
});
|
|
98
|
+
it("skips malformed descriptors (fail-soft)", () => {
|
|
99
|
+
const malformed = slotRow({ descriptor: { key: "broken" } });
|
|
100
|
+
const result = assembleInstalledExtensions([malformed], localizations, { activeLocale: "en" });
|
|
101
|
+
expect(result.slots).toHaveLength(0);
|
|
102
|
+
expect(result.pages).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
it("gives each installation a unique descriptor key", () => {
|
|
105
|
+
const result = assembleInstalledExtensions([slotRow(), slotRow({ installationId: "apin_2" })], localizations, { activeLocale: "en" });
|
|
106
|
+
expect(result.slots.map((slot) => slot.descriptor.key)).toEqual([
|
|
107
|
+
"apin_1:reports",
|
|
108
|
+
"apin_2:reports",
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -5,12 +5,17 @@ export * from "./app-api-service.js";
|
|
|
5
5
|
export * from "./compiler.js";
|
|
6
6
|
export * from "./consent.js";
|
|
7
7
|
export * from "./contracts.js";
|
|
8
|
+
export * from "./extension-resolution.js";
|
|
8
9
|
export * from "./ingestion.js";
|
|
10
|
+
export * from "./installation-read-model.js";
|
|
9
11
|
export * from "./installation-service.js";
|
|
12
|
+
export * from "./locale-resolution.js";
|
|
10
13
|
export * from "./oauth-crypto.js";
|
|
11
14
|
export * from "./oauth-service.js";
|
|
12
15
|
export { createAppsAdminRoutes } from "./routes.js";
|
|
13
|
-
export { appAccessCredentialStatusEnum, appAccessCredentials, appAccessTokenModeEnum, appAuditEventKindEnum, appAuditEvents, appCredentialKindEnum, appCredentials, appDistributionEnum, appExtensionInstallations, appGrantStatusEnum, appGrants, appInstallationRegistrationStatusEnum, appInstallationSettings, appInstallationStatusEnum, appInstallations, appInstallationUpdatePolicyEnum, appLifecycleStateEnum, appOAuthAuthorizationCodes, appOAuthRefreshTokens, appRedirectUris, appReleaseArtifactStateEnum, appReleaseArtifacts, appReleaseLocalizations, appReleaseStateEnum, appReleases, appSecretReferences, apps, appWebhookSubscriptionStatusEnum, appWebhookSubscriptions, } from "./schema.js";
|
|
16
|
+
export { appAccessCredentialStatusEnum, appAccessCredentials, appAccessTokenModeEnum, appAuditEventKindEnum, appAuditEvents, appCredentialKindEnum, appCredentials, appDistributionEnum, appExtensionInstallations, appGrantStatusEnum, appGrants, appInstallationRegistrationStatusEnum, appInstallationSettings, appInstallationStatusEnum, appInstallations, appInstallationUpdatePolicyEnum, appLifecycleStateEnum, appOAuthAuthorizationCodes, appOAuthRefreshTokens, appRedirectUris, appReleaseArtifactStateEnum, appReleaseArtifacts, appReleaseLocalizations, appReleaseStateEnum, appReleases, appSecretReferences, appSessionTokens, apps, appWebhookSubscriptionStatusEnum, appWebhookSubscriptions, } from "./schema.js";
|
|
14
17
|
export { createAppsService } from "./service.js";
|
|
18
|
+
export * from "./session-token.js";
|
|
19
|
+
export * from "./session-token-service.js";
|
|
15
20
|
export type { AppWebhookDeliveryOptions } from "./webhook-delivery.js";
|
|
16
21
|
export { createAppWebhookDeliveryStore, createAppWebhookEventQueue, enqueueAppWebhookEvent, listAppWebhookHealth, replayAppWebhookDelivery, } from "./webhook-delivery.js";
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,16 @@ export * from "./app-api-service.js";
|
|
|
5
5
|
export * from "./compiler.js";
|
|
6
6
|
export * from "./consent.js";
|
|
7
7
|
export * from "./contracts.js";
|
|
8
|
+
export * from "./extension-resolution.js";
|
|
8
9
|
export * from "./ingestion.js";
|
|
10
|
+
export * from "./installation-read-model.js";
|
|
9
11
|
export * from "./installation-service.js";
|
|
12
|
+
export * from "./locale-resolution.js";
|
|
10
13
|
export * from "./oauth-crypto.js";
|
|
11
14
|
export * from "./oauth-service.js";
|
|
12
15
|
export { createAppsAdminRoutes } from "./routes.js";
|
|
13
|
-
export { appAccessCredentialStatusEnum, appAccessCredentials, appAccessTokenModeEnum, appAuditEventKindEnum, appAuditEvents, appCredentialKindEnum, appCredentials, appDistributionEnum, appExtensionInstallations, appGrantStatusEnum, appGrants, appInstallationRegistrationStatusEnum, appInstallationSettings, appInstallationStatusEnum, appInstallations, appInstallationUpdatePolicyEnum, appLifecycleStateEnum, appOAuthAuthorizationCodes, appOAuthRefreshTokens, appRedirectUris, appReleaseArtifactStateEnum, appReleaseArtifacts, appReleaseLocalizations, appReleaseStateEnum, appReleases, appSecretReferences, apps, appWebhookSubscriptionStatusEnum, appWebhookSubscriptions, } from "./schema.js";
|
|
16
|
+
export { appAccessCredentialStatusEnum, appAccessCredentials, appAccessTokenModeEnum, appAuditEventKindEnum, appAuditEvents, appCredentialKindEnum, appCredentials, appDistributionEnum, appExtensionInstallations, appGrantStatusEnum, appGrants, appInstallationRegistrationStatusEnum, appInstallationSettings, appInstallationStatusEnum, appInstallations, appInstallationUpdatePolicyEnum, appLifecycleStateEnum, appOAuthAuthorizationCodes, appOAuthRefreshTokens, appRedirectUris, appReleaseArtifactStateEnum, appReleaseArtifacts, appReleaseLocalizations, appReleaseStateEnum, appReleases, appSecretReferences, appSessionTokens, apps, appWebhookSubscriptionStatusEnum, appWebhookSubscriptions, } from "./schema.js";
|
|
14
17
|
export { createAppsService } from "./service.js";
|
|
18
|
+
export * from "./session-token.js";
|
|
19
|
+
export * from "./session-token-service.js";
|
|
15
20
|
export { createAppWebhookDeliveryStore, createAppWebhookEventQueue, enqueueAppWebhookEvent, listAppWebhookHealth, replayAppWebhookDelivery, } from "./webhook-delivery.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import type { AppInstallationListQuery } from "./contracts.js";
|
|
3
|
+
import { type AppInstallation, type AppRegistration, type AppRelease, appAuditEvents, appExtensionInstallations, appGrants } from "./schema.js";
|
|
4
|
+
import { listAppWebhookHealth } from "./webhook-delivery.js";
|
|
5
|
+
export type AppGrantRow = typeof appGrants.$inferSelect;
|
|
6
|
+
export type AppExtensionInstallationRow = typeof appExtensionInstallations.$inferSelect;
|
|
7
|
+
export type AppAuditEventRow = typeof appAuditEvents.$inferSelect;
|
|
8
|
+
export type AppWebhookHealth = Awaited<ReturnType<typeof listAppWebhookHealth>>;
|
|
9
|
+
/**
|
|
10
|
+
* A single installation as shown in a governance list: the installation row
|
|
11
|
+
* plus the joined app and active-release descriptors the UI renders per row.
|
|
12
|
+
*/
|
|
13
|
+
export interface InstallationSummary extends AppInstallation {
|
|
14
|
+
appDisplayName: string;
|
|
15
|
+
appSlug: string;
|
|
16
|
+
distribution: AppRegistration["distribution"];
|
|
17
|
+
releaseVersion: string;
|
|
18
|
+
}
|
|
19
|
+
export interface InstallationListResult {
|
|
20
|
+
data: InstallationSummary[];
|
|
21
|
+
total: number;
|
|
22
|
+
limit: number;
|
|
23
|
+
offset: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A release the installation could move to, with whether governance can apply
|
|
27
|
+
* it without new consent and why it is blocked when it cannot.
|
|
28
|
+
*/
|
|
29
|
+
export interface AppAvailableUpdate {
|
|
30
|
+
release: AppRelease;
|
|
31
|
+
blocked: boolean;
|
|
32
|
+
blockedReason: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface InstallationDetail {
|
|
35
|
+
installation: AppInstallation;
|
|
36
|
+
app: AppRegistration;
|
|
37
|
+
activeRelease: AppRelease;
|
|
38
|
+
pendingRelease: AppRelease | null;
|
|
39
|
+
pendingReason: string | null;
|
|
40
|
+
grants: AppGrantRow[];
|
|
41
|
+
extensions: AppExtensionInstallationRow[];
|
|
42
|
+
webhooks: AppWebhookHealth;
|
|
43
|
+
recentAudit: AppAuditEventRow[];
|
|
44
|
+
availableUpdates: AppAvailableUpdate[];
|
|
45
|
+
}
|
|
46
|
+
export interface AvailableUpdateOptions {
|
|
47
|
+
platformApiVersion?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function listInstallationSummaries(db: PostgresJsDatabase, query: AppInstallationListQuery): Promise<InstallationListResult>;
|
|
50
|
+
export declare function listAppReleases(db: PostgresJsDatabase, appId: string): Promise<AppRelease[]>;
|
|
51
|
+
export declare function listInstallationAudit(db: PostgresJsDatabase, installationId: string, limit: number): Promise<AppAuditEventRow[]>;
|
|
52
|
+
export declare function loadInstallationDetail(db: PostgresJsDatabase, installationId: string, options?: AvailableUpdateOptions): Promise<InstallationDetail | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Pure evaluation of which other available releases a governance operator can
|
|
55
|
+
* roll to. A candidate is blocked when it requires scopes the installation has
|
|
56
|
+
* not already granted, or when it falls outside the platform API range (only
|
|
57
|
+
* checked when a platform API version is known).
|
|
58
|
+
*/
|
|
59
|
+
export declare function computeAvailableUpdates(input: {
|
|
60
|
+
installation: Pick<AppInstallation, "releaseId">;
|
|
61
|
+
candidateReleases: readonly AppRelease[];
|
|
62
|
+
grantedScopes: ReadonlySet<string>;
|
|
63
|
+
platformApiVersion?: string;
|
|
64
|
+
}): AppAvailableUpdate[];
|