@voyant-travel/apps 0.5.0 → 0.6.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.
@@ -0,0 +1,195 @@
1
+ import { createRoute, z } from "@hono/zod-openapi";
2
+ import { activateInstallationBodySchema, appCredentialRevocationSchema, appInstallationAuditQuerySchema, appInstallationListQuerySchema, appListQuerySchema, appOAuthAuthorizeQuerySchema, appOAuthTokenSchema, appSessionTokenExchangeSchema, appSessionTokenIssueSchema, appWebhookReplaySchema, createCustomAppRegistrationSchema, installAppSchema, lifecycleActionBodySchema, releaseManifestFetchSchema, releaseManifestUploadSchema, } from "./contracts.js";
3
+ const appIdParamSchema = z.object({ appId: z.string().min(1) });
4
+ const installationIdParamSchema = z.object({ installationId: z.string().min(1) });
5
+ const errorSchema = z.object({ error: z.string() });
6
+ const dataEnvelopeSchema = z.object({ data: z.unknown() });
7
+ const listEnvelopeSchema = z.object({
8
+ data: z.array(z.unknown()),
9
+ total: z.number(),
10
+ limit: z.number(),
11
+ offset: z.number(),
12
+ });
13
+ const releaseCreateResponseSchema = z.object({
14
+ data: z.unknown(),
15
+ digest: z.string(),
16
+ created: z.boolean(),
17
+ });
18
+ const oauthRedirectResponseSchema = z.object({
19
+ data: z.object({ redirectUrl: z.string().url(), state: z.string() }),
20
+ });
21
+ const tokenResponseSchema = z.record(z.string(), z.unknown());
22
+ const credentialRevocationResponseSchema = z.object({
23
+ installationId: z.string(),
24
+ generation: z.number(),
25
+ });
26
+ const jsonContent = (schema, description) => ({
27
+ description,
28
+ content: { "application/json": { schema } },
29
+ });
30
+ const requiredJsonBody = (schema) => ({
31
+ required: true,
32
+ content: { "application/json": { schema } },
33
+ });
34
+ export const listAppsRoute = createRoute({
35
+ method: "get",
36
+ path: "/",
37
+ request: { query: appListQuerySchema },
38
+ responses: { 200: jsonContent(listEnvelopeSchema, "Paginated app registrations") },
39
+ });
40
+ export const createCustomAppRoute = createRoute({
41
+ method: "post",
42
+ path: "/",
43
+ request: { body: requiredJsonBody(createCustomAppRegistrationSchema) },
44
+ responses: {
45
+ 201: jsonContent(dataEnvelopeSchema, "Created custom app registration"),
46
+ 409: jsonContent(errorSchema, "Duplicate app registration"),
47
+ },
48
+ });
49
+ export const authorizeAppOAuthRoute = createRoute({
50
+ method: "post",
51
+ path: "/oauth/authorize",
52
+ request: { body: requiredJsonBody(appOAuthAuthorizeQuerySchema) },
53
+ responses: {
54
+ 200: jsonContent(oauthRedirectResponseSchema, "OAuth authorization redirect target"),
55
+ 501: jsonContent(errorSchema, "App OAuth is not configured"),
56
+ },
57
+ });
58
+ export const issueAppOAuthTokenRoute = createRoute({
59
+ method: "post",
60
+ path: "/oauth/token",
61
+ request: { body: requiredJsonBody(appOAuthTokenSchema) },
62
+ responses: {
63
+ 200: jsonContent(tokenResponseSchema, "OAuth token response"),
64
+ 501: jsonContent(errorSchema, "App OAuth is not configured"),
65
+ },
66
+ });
67
+ export const revokeAppInstallationCredentialsRoute = createRoute({
68
+ method: "post",
69
+ path: "/oauth/revoke-installation",
70
+ request: { body: requiredJsonBody(appCredentialRevocationSchema) },
71
+ responses: {
72
+ 200: jsonContent(credentialRevocationResponseSchema, "Revoked installation credential generation"),
73
+ 501: jsonContent(errorSchema, "App OAuth is not configured"),
74
+ },
75
+ });
76
+ export const issueAppSessionTokenRoute = createRoute({
77
+ method: "post",
78
+ path: "/installations/{installationId}/session-token",
79
+ request: {
80
+ params: installationIdParamSchema,
81
+ body: requiredJsonBody(appSessionTokenIssueSchema),
82
+ },
83
+ responses: {
84
+ 201: jsonContent(dataEnvelopeSchema, "Issued app session token"),
85
+ 501: jsonContent(errorSchema, "App session tokens are not configured"),
86
+ },
87
+ });
88
+ export const exchangeAppSessionTokenRoute = createRoute({
89
+ method: "post",
90
+ path: "/oauth/session-token/exchange",
91
+ request: { body: requiredJsonBody(appSessionTokenExchangeSchema) },
92
+ responses: {
93
+ 200: jsonContent(tokenResponseSchema, "Online app token response"),
94
+ 501: jsonContent(errorSchema, "App session tokens are not configured"),
95
+ },
96
+ });
97
+ export const installAppRoute = createRoute({
98
+ method: "post",
99
+ path: "/install",
100
+ request: { body: requiredJsonBody(installAppSchema) },
101
+ responses: { 201: jsonContent(dataEnvelopeSchema, "Installed app") },
102
+ });
103
+ export const listAppInstallationsRoute = createRoute({
104
+ method: "get",
105
+ path: "/installations",
106
+ request: { query: appInstallationListQuerySchema },
107
+ responses: { 200: jsonContent(listEnvelopeSchema, "Paginated app installations") },
108
+ });
109
+ export const getAppInstallationRoute = createRoute({
110
+ method: "get",
111
+ path: "/installations/{installationId}",
112
+ request: { params: installationIdParamSchema },
113
+ responses: {
114
+ 200: jsonContent(dataEnvelopeSchema, "App installation detail"),
115
+ 404: jsonContent(errorSchema, "App installation not found"),
116
+ },
117
+ });
118
+ export const listAppInstallationAuditRoute = createRoute({
119
+ method: "get",
120
+ path: "/installations/{installationId}/audit",
121
+ request: { params: installationIdParamSchema, query: appInstallationAuditQuerySchema },
122
+ responses: { 200: jsonContent(dataEnvelopeSchema, "App installation audit events") },
123
+ });
124
+ export const pauseAppInstallationRoute = createRoute({
125
+ method: "post",
126
+ path: "/installations/{installationId}/pause",
127
+ request: { params: installationIdParamSchema, body: requiredJsonBody(lifecycleActionBodySchema) },
128
+ responses: { 200: jsonContent(dataEnvelopeSchema, "Paused app installation") },
129
+ });
130
+ export const resumeAppInstallationRoute = createRoute({
131
+ method: "post",
132
+ path: "/installations/{installationId}/resume",
133
+ request: { params: installationIdParamSchema, body: requiredJsonBody(lifecycleActionBodySchema) },
134
+ responses: { 200: jsonContent(dataEnvelopeSchema, "Resumed app installation") },
135
+ });
136
+ export const uninstallAppInstallationRoute = createRoute({
137
+ method: "post",
138
+ path: "/installations/{installationId}/uninstall",
139
+ request: { params: installationIdParamSchema, body: requiredJsonBody(lifecycleActionBodySchema) },
140
+ responses: { 200: jsonContent(dataEnvelopeSchema, "Uninstalled app installation") },
141
+ });
142
+ export const activateAppInstallationRoute = createRoute({
143
+ method: "post",
144
+ path: "/installations/{installationId}/activate",
145
+ request: {
146
+ params: installationIdParamSchema,
147
+ body: requiredJsonBody(activateInstallationBodySchema),
148
+ },
149
+ responses: { 200: jsonContent(dataEnvelopeSchema, "Activated app installation release") },
150
+ });
151
+ export const previewAppInstallationPurgeRoute = createRoute({
152
+ method: "post",
153
+ path: "/installations/{installationId}/purge-preview",
154
+ request: { params: installationIdParamSchema, body: requiredJsonBody(lifecycleActionBodySchema) },
155
+ responses: { 200: jsonContent(dataEnvelopeSchema, "App installation purge preview") },
156
+ });
157
+ export const getAppRoute = createRoute({
158
+ method: "get",
159
+ path: "/{appId}",
160
+ request: { params: appIdParamSchema },
161
+ responses: {
162
+ 200: jsonContent(dataEnvelopeSchema, "App registration"),
163
+ 404: jsonContent(errorSchema, "App not found"),
164
+ },
165
+ });
166
+ export const listAppReleasesRoute = createRoute({
167
+ method: "get",
168
+ path: "/{appId}/releases",
169
+ request: { params: appIdParamSchema },
170
+ responses: { 200: jsonContent(dataEnvelopeSchema, "App releases") },
171
+ });
172
+ export const createAppReleaseRoute = createRoute({
173
+ method: "post",
174
+ path: "/{appId}/releases",
175
+ request: { params: appIdParamSchema, body: requiredJsonBody(releaseManifestUploadSchema) },
176
+ responses: { 201: jsonContent(releaseCreateResponseSchema, "Created app release") },
177
+ });
178
+ export const fetchAppReleaseRoute = createRoute({
179
+ method: "post",
180
+ path: "/{appId}/releases/fetch",
181
+ request: { params: appIdParamSchema, body: requiredJsonBody(releaseManifestFetchSchema) },
182
+ responses: { 201: jsonContent(releaseCreateResponseSchema, "Fetched app release") },
183
+ });
184
+ export const listAppWebhooksRoute = createRoute({
185
+ method: "get",
186
+ path: "/installations/{installationId}/webhooks",
187
+ request: { params: installationIdParamSchema },
188
+ responses: { 200: jsonContent(dataEnvelopeSchema, "App webhook delivery health") },
189
+ });
190
+ export const replayAppWebhookRoute = createRoute({
191
+ method: "post",
192
+ path: "/installations/{installationId}/webhooks/replay",
193
+ request: { params: installationIdParamSchema, body: requiredJsonBody(appWebhookReplaySchema) },
194
+ responses: { 202: jsonContent(dataEnvelopeSchema, "Replayed app webhook delivery") },
195
+ });
package/dist/routes.d.ts CHANGED
@@ -1,8 +1,15 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { EventBus } from "@voyant-travel/core/events";
3
+ import type { createCustomFieldsService } from "@voyant-travel/custom-fields";
1
4
  import type { AccessCatalog } from "@voyant-travel/types/api-keys";
2
5
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
- import { Hono } from "hono";
4
6
  import { type AppsServiceOptions } from "./service.js";
7
+ type CustomFieldsService = ReturnType<typeof createCustomFieldsService>;
5
8
  type Env = {
9
+ Bindings: {
10
+ /** Deployment identity; the install lifecycle is scoped to it. */
11
+ VOYANT_CLOUD_DEPLOYMENT_ID?: string;
12
+ };
6
13
  Variables: {
7
14
  db: PostgresJsDatabase;
8
15
  };
@@ -21,6 +28,15 @@ export interface AppsAdminRouteOptions extends AppsServiceOptions {
21
28
  secret: string;
22
29
  ttlSeconds?: number;
23
30
  };
31
+ /**
32
+ * Deployment identity used when installing over HTTP. Falls back to
33
+ * {@link oauth}'s deployment id when omitted.
34
+ */
35
+ deploymentId?: string;
36
+ /** Platform API version used to gate release compatibility. */
37
+ platformApiVersion?: string;
38
+ eventBus?: EventBus;
39
+ customFields?: CustomFieldsService;
24
40
  }
25
- export declare function createAppsAdminRoutes(options?: AppsAdminRouteOptions): Hono<Env, import("hono/types").BlankSchema, "/">;
41
+ export declare function createAppsAdminRoutes(options?: AppsAdminRouteOptions): OpenAPIHono<Env, {}, "/">;
26
42
  export {};
package/dist/routes.js CHANGED
@@ -1,16 +1,25 @@
1
- import { parseJsonBody, parseQuery, RequestValidationError, requireUserId, } from "@voyant-travel/hono";
2
- import { Hono } from "hono";
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { openApiValidationHook, parseJsonBody, parseQuery, RequestValidationError, requireUserId, } from "@voyant-travel/hono";
3
3
  import { z } from "zod";
4
- import { appCredentialRevocationSchema, appListQuerySchema, appOAuthAuthorizeQuerySchema, appOAuthTokenSchema, appSessionTokenExchangeSchema, appSessionTokenIssueSchema, appWebhookReplaySchema, createCustomAppRegistrationSchema, releaseManifestFetchSchema, releaseManifestUploadSchema, } from "./contracts.js";
4
+ import { activateInstallationBodySchema, appCredentialRevocationSchema, appInstallationAuditQuerySchema, appInstallationListQuerySchema, appListQuerySchema, appOAuthAuthorizeQuerySchema, appOAuthTokenSchema, appSessionTokenExchangeSchema, appSessionTokenIssueSchema, appWebhookReplaySchema, createCustomAppRegistrationSchema, installAppSchema, lifecycleActionBodySchema, releaseManifestFetchSchema, releaseManifestUploadSchema, } from "./contracts.js";
5
+ import { listAppReleases, listInstallationAudit, listInstallationSummaries, loadInstallationDetail, } from "./installation-read-model.js";
6
+ import { createAppInstallationService } from "./installation-service.js";
5
7
  import { createAppOAuthService } from "./oauth-service.js";
8
+ import { activateAppInstallationRoute, authorizeAppOAuthRoute, createAppReleaseRoute, createCustomAppRoute, exchangeAppSessionTokenRoute, fetchAppReleaseRoute, getAppInstallationRoute, getAppRoute, installAppRoute, issueAppOAuthTokenRoute, issueAppSessionTokenRoute, listAppInstallationAuditRoute, listAppInstallationsRoute, listAppReleasesRoute, listAppsRoute, listAppWebhooksRoute, pauseAppInstallationRoute, previewAppInstallationPurgeRoute, replayAppWebhookRoute, resumeAppInstallationRoute, revokeAppInstallationCredentialsRoute, uninstallAppInstallationRoute, } from "./routes-openapi.js";
6
9
  import { createAppsService } from "./service.js";
7
10
  import { createAppSessionTokenService } from "./session-token-service.js";
8
11
  import { listAppWebhookHealth, replayAppWebhookDelivery } from "./webhook-delivery.js";
9
12
  const appIdParamSchema = z.object({ appId: z.string().min(1) });
10
13
  const installationIdParamSchema = z.object({ installationId: z.string().min(1) });
11
14
  export function createAppsAdminRoutes(options = {}) {
12
- const routes = new Hono();
15
+ const routes = new OpenAPIHono({ defaultHook: openApiValidationHook });
13
16
  const service = createAppsService(options);
17
+ const installations = createAppInstallationService({
18
+ deploymentId: options.oauth?.deploymentId ?? options.deploymentId,
19
+ platformApiVersion: options.platformApiVersion,
20
+ eventBus: options.eventBus,
21
+ customFields: options.customFields,
22
+ });
14
23
  const oauth = options.oauth ? createAppOAuthService(options.oauth) : null;
15
24
  const sessionTokens = oauth && options.oauth && options.sessionToken
16
25
  ? createAppSessionTokenService({
@@ -20,11 +29,11 @@ export function createAppsAdminRoutes(options = {}) {
20
29
  oauth,
21
30
  })
22
31
  : null;
23
- routes.get("/", async (c) => {
32
+ routes.openapi(listAppsRoute, async (c) => {
24
33
  const query = parseQuery(c, appListQuerySchema);
25
34
  return c.json(await service.list(c.get("db"), query), 200);
26
35
  });
27
- routes.post("/", async (c) => {
36
+ routes.openapi(createCustomAppRoute, async (c) => {
28
37
  const body = await parseJsonBody(c, createCustomAppRegistrationSchema);
29
38
  const app = await service.createCustomApp(c.get("db"), body);
30
39
  return c.json({ data: app }, 201);
@@ -32,7 +41,7 @@ export function createAppsAdminRoutes(options = {}) {
32
41
  // Consent approval mutates state (installation, grants, authorization code),
33
42
  // so it must never be reachable through read-scoped GET requests. The admin
34
43
  // consent UI submits the approval and performs the redirect itself.
35
- routes.post("/oauth/authorize", async (c) => {
44
+ routes.openapi(authorizeAppOAuthRoute, async (c) => {
36
45
  if (!oauth)
37
46
  return c.json({ error: "App OAuth is not configured" }, 501);
38
47
  const body = await parseJsonBody(c, appOAuthAuthorizeQuerySchema);
@@ -52,7 +61,7 @@ export function createAppsAdminRoutes(options = {}) {
52
61
  redirectUrl.searchParams.set("state", result.state);
53
62
  return c.json({ data: { redirectUrl: redirectUrl.toString(), state: result.state } }, 200);
54
63
  });
55
- routes.post("/oauth/token", async (c) => {
64
+ routes.openapi(issueAppOAuthTokenRoute, async (c) => {
56
65
  if (!oauth)
57
66
  return c.json({ error: "App OAuth is not configured" }, 501);
58
67
  const body = await parseJsonBody(c, appOAuthTokenSchema);
@@ -83,7 +92,7 @@ export function createAppsAdminRoutes(options = {}) {
83
92
  });
84
93
  return c.json(token, 200);
85
94
  });
86
- routes.post("/oauth/revoke-installation", async (c) => {
95
+ routes.openapi(revokeAppInstallationCredentialsRoute, async (c) => {
87
96
  if (!oauth)
88
97
  return c.json({ error: "App OAuth is not configured" }, 501);
89
98
  const body = await parseJsonBody(c, appCredentialRevocationSchema);
@@ -93,7 +102,7 @@ export function createAppsAdminRoutes(options = {}) {
93
102
  // Staff-authenticated: the admin host requests a short-lived session token for
94
103
  // the current viewer + entity/slot context. The viewer is taken from the
95
104
  // authenticated session, never from the frame.
96
- routes.post("/installations/:installationId/session-token", async (c) => {
105
+ routes.openapi(issueAppSessionTokenRoute, async (c) => {
97
106
  if (!sessionTokens)
98
107
  return c.json({ error: "App session tokens are not configured" }, 501);
99
108
  const { installationId } = parseInstallationParams(c.req.param());
@@ -109,7 +118,7 @@ export function createAppsAdminRoutes(options = {}) {
109
118
  });
110
119
  // App-backend-facing: exchange a presented session token for online actor
111
120
  // access. Client-authenticated; bounded by viewer ∩ app grants.
112
- routes.post("/oauth/session-token/exchange", async (c) => {
121
+ routes.openapi(exchangeAppSessionTokenRoute, async (c) => {
113
122
  if (!sessionTokens)
114
123
  return c.json({ error: "App session tokens are not configured" }, 501);
115
124
  const body = await parseJsonBody(c, appSessionTokenExchangeSchema);
@@ -122,28 +131,112 @@ export function createAppsAdminRoutes(options = {}) {
122
131
  });
123
132
  return c.json(token, 200);
124
133
  });
125
- routes.get("/:appId", async (c) => {
134
+ routes.openapi(installAppRoute, async (c) => {
135
+ const body = await parseJsonBody(c, installAppSchema);
136
+ // The deployment id is a runtime value (not known at graph-composition
137
+ // time), so resolve it per request: explicit body → runtime env →
138
+ // construction option. Without this the standard runtime mounts these
139
+ // routes with no deployment id and every install 400s (app_deployment_required).
140
+ const deploymentId = body.deploymentId ?? c.env?.VOYANT_CLOUD_DEPLOYMENT_ID?.trim() ?? options.deploymentId;
141
+ const result = await installations.install(c.get("db"), {
142
+ appId: body.appId,
143
+ releaseId: body.releaseId,
144
+ actorId: body.actorId,
145
+ grantedOptionalScopes: body.grantedOptionalScopes,
146
+ updatePolicy: body.updatePolicy,
147
+ deploymentId,
148
+ });
149
+ return c.json({ data: result }, 201);
150
+ });
151
+ routes.openapi(listAppInstallationsRoute, async (c) => {
152
+ const query = parseQuery(c, appInstallationListQuerySchema);
153
+ return c.json(await listInstallationSummaries(c.get("db"), query), 200);
154
+ });
155
+ routes.openapi(getAppInstallationRoute, async (c) => {
156
+ const { installationId } = parseInstallationParams(c.req.param());
157
+ const detail = await loadInstallationDetail(c.get("db"), installationId, {
158
+ platformApiVersion: options.platformApiVersion,
159
+ });
160
+ return detail
161
+ ? c.json({ data: detail }, 200)
162
+ : c.json({ error: "App installation not found" }, 404);
163
+ });
164
+ routes.openapi(listAppInstallationAuditRoute, async (c) => {
165
+ const { installationId } = parseInstallationParams(c.req.param());
166
+ const query = parseQuery(c, appInstallationAuditQuerySchema);
167
+ const data = await listInstallationAudit(c.get("db"), installationId, query.limit);
168
+ return c.json({ data }, 200);
169
+ });
170
+ routes.openapi(pauseAppInstallationRoute, async (c) => {
171
+ const { installationId } = parseInstallationParams(c.req.param());
172
+ const body = await parseJsonBody(c, lifecycleActionBodySchema);
173
+ const result = await installations.pause(c.get("db"), { installationId, actorId: body.actorId });
174
+ return c.json({ data: result }, 200);
175
+ });
176
+ routes.openapi(resumeAppInstallationRoute, async (c) => {
177
+ const { installationId } = parseInstallationParams(c.req.param());
178
+ const body = await parseJsonBody(c, lifecycleActionBodySchema);
179
+ const result = await installations.resume(c.get("db"), {
180
+ installationId,
181
+ actorId: body.actorId,
182
+ });
183
+ return c.json({ data: result }, 200);
184
+ });
185
+ routes.openapi(uninstallAppInstallationRoute, async (c) => {
186
+ const { installationId } = parseInstallationParams(c.req.param());
187
+ const body = await parseJsonBody(c, lifecycleActionBodySchema);
188
+ const result = await installations.uninstall(c.get("db"), {
189
+ installationId,
190
+ actorId: body.actorId,
191
+ });
192
+ return c.json({ data: result }, 200);
193
+ });
194
+ routes.openapi(activateAppInstallationRoute, async (c) => {
195
+ const { installationId } = parseInstallationParams(c.req.param());
196
+ const body = await parseJsonBody(c, activateInstallationBodySchema);
197
+ const result = await installations.upgrade(c.get("db"), {
198
+ installationId,
199
+ releaseId: body.releaseId,
200
+ actorId: body.actorId,
201
+ });
202
+ return c.json({ data: result }, 200);
203
+ });
204
+ routes.openapi(previewAppInstallationPurgeRoute, async (c) => {
205
+ const { installationId } = parseInstallationParams(c.req.param());
206
+ const body = await parseJsonBody(c, lifecycleActionBodySchema);
207
+ const result = await installations.purgePreview(c.get("db"), {
208
+ installationId,
209
+ actorId: body.actorId,
210
+ });
211
+ return c.json({ data: result }, 200);
212
+ });
213
+ routes.openapi(getAppRoute, async (c) => {
126
214
  const { appId } = parseParams(c.req.param());
127
215
  const app = await service.get(c.get("db"), appId);
128
216
  return app ? c.json({ data: app }, 200) : c.json({ error: "App not found" }, 404);
129
217
  });
130
- routes.post("/:appId/releases", async (c) => {
218
+ routes.openapi(listAppReleasesRoute, async (c) => {
219
+ const { appId } = parseParams(c.req.param());
220
+ const data = await listAppReleases(c.get("db"), appId);
221
+ return c.json({ data }, 200);
222
+ });
223
+ routes.openapi(createAppReleaseRoute, async (c) => {
131
224
  const { appId } = parseParams(c.req.param());
132
225
  const body = await parseJsonBody(c, releaseManifestUploadSchema);
133
226
  const result = await service.releaseFromUpload(c.get("db"), appId, body);
134
227
  return c.json({ data: result.release, digest: result.digest, created: result.created }, 201);
135
228
  });
136
- routes.post("/:appId/releases/fetch", async (c) => {
229
+ routes.openapi(fetchAppReleaseRoute, async (c) => {
137
230
  const { appId } = parseParams(c.req.param());
138
231
  const body = await parseJsonBody(c, releaseManifestFetchSchema);
139
232
  const result = await service.releaseFromFetch(c.get("db"), appId, body);
140
233
  return c.json({ data: result.release, digest: result.digest, created: result.created }, 201);
141
234
  });
142
- routes.get("/installations/:installationId/webhooks", async (c) => {
235
+ routes.openapi(listAppWebhooksRoute, async (c) => {
143
236
  const { installationId } = parseInstallationParams(c.req.param());
144
237
  return c.json(await listAppWebhookHealth(c.get("db"), installationId), 200);
145
238
  });
146
- routes.post("/installations/:installationId/webhooks/replay", async (c) => {
239
+ routes.openapi(replayAppWebhookRoute, async (c) => {
147
240
  parseInstallationParams(c.req.param());
148
241
  const body = await parseJsonBody(c, appWebhookReplaySchema);
149
242
  const delivery = await replayAppWebhookDelivery(c.get("db"), {
@@ -0,0 +1 @@
1
+ export declare function createAppsRuntimePortContribution(): Readonly<Record<string, unknown>>;
@@ -0,0 +1,3 @@
1
+ export function createAppsRuntimePortContribution() {
2
+ return {};
3
+ }
package/dist/voyant.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import { defineModule, requirePort } from "@voyant-travel/core/project";
2
2
  import { customFieldValueLifecycleRuntimePort, customFieldValueOperationsRuntimePort, } from "@voyant-travel/core/runtime-port";
3
+ const appsAdminRuntime = {
4
+ entry: "@voyant-travel/apps-react/admin",
5
+ export: "createSelectedAppsAdminExtension",
6
+ };
3
7
  const appInstallationLifecyclePayloadSchema = {
4
8
  type: "object",
5
9
  additionalProperties: false,
@@ -29,6 +33,7 @@ export const appsVoyantModule = defineModule({
29
33
  id: "@voyant-travel/apps#api.admin",
30
34
  surface: "admin",
31
35
  mount: "apps",
36
+ openapi: { document: "apps" },
32
37
  resource: "apps",
33
38
  transactional: true,
34
39
  runtime: {
@@ -107,7 +112,18 @@ export const appsVoyantModule = defineModule({
107
112
  label: "Own custom-field definitions",
108
113
  description: "Read and write custom-field definitions owned by the app.",
109
114
  remoteSafe: true,
110
- actions: ["read", "write"],
115
+ actions: [
116
+ {
117
+ action: "read",
118
+ label: "Read custom-field definitions",
119
+ description: "Read definitions owned by the app.",
120
+ },
121
+ {
122
+ action: "write",
123
+ label: "Write custom-field definitions",
124
+ description: "Create and update definitions owned by the app.",
125
+ },
126
+ ],
111
127
  wildcard: "explicit-resource",
112
128
  },
113
129
  {
@@ -116,17 +132,55 @@ export const appsVoyantModule = defineModule({
116
132
  label: "Own custom-field values",
117
133
  description: "Read and write app-owned custom-field values by target.",
118
134
  remoteSafe: true,
119
- actions: ["read", "write", "booking", "person", "organization", "invoice"],
135
+ actions: [
136
+ {
137
+ action: "read",
138
+ label: "Read custom-field values",
139
+ description: "Read custom-field values owned by the app.",
140
+ },
141
+ {
142
+ action: "write",
143
+ label: "Write custom-field values",
144
+ description: "Write custom-field values owned by the app.",
145
+ },
146
+ {
147
+ action: "booking",
148
+ label: "Booking custom fields",
149
+ description: "Access app-owned custom-field values on bookings.",
150
+ },
151
+ {
152
+ action: "person",
153
+ label: "Person custom fields",
154
+ description: "Access app-owned custom-field values on people.",
155
+ },
156
+ {
157
+ action: "organization",
158
+ label: "Organization custom fields",
159
+ description: "Access app-owned custom-field values on organizations.",
160
+ },
161
+ {
162
+ action: "invoice",
163
+ label: "Invoice custom fields",
164
+ description: "Access app-owned custom-field values on invoices.",
165
+ },
166
+ ],
120
167
  wildcard: "explicit-resource",
121
168
  },
122
169
  {
123
- id: "@voyant-travel/apps#access.webhooks",
124
- resource: "webhooks",
170
+ id: "@voyant-travel/apps#access.app-webhooks",
171
+ // Namespaced `app-webhooks` (not `webhooks`) to avoid a duplicate
172
+ // access-resource authority with @voyant-travel/workflow-runs, which
173
+ // owns the deployment `webhooks` resource.
174
+ resource: "app-webhooks",
125
175
  label: "App webhooks",
126
176
  description: "Read webhook health and request replay.",
127
177
  remoteSafe: true,
128
178
  actions: [
129
- "read",
179
+ {
180
+ action: "read",
181
+ label: "Read app webhooks",
182
+ description: "Read app webhook subscription and delivery health.",
183
+ },
130
184
  { action: "replay", label: "Replay webhooks", description: "Replay deliveries." },
131
185
  ],
132
186
  },
@@ -136,7 +190,13 @@ export const appsVoyantModule = defineModule({
136
190
  label: "App audit history",
137
191
  description: "Read audit history owned by the app.",
138
192
  remoteSafe: true,
139
- actions: ["read"],
193
+ actions: [
194
+ {
195
+ action: "read",
196
+ label: "Read app audit history",
197
+ description: "Read audit events owned by the app.",
198
+ },
199
+ ],
140
200
  },
141
201
  {
142
202
  id: "@voyant-travel/apps#access.online-token",
@@ -144,7 +204,14 @@ export const appsVoyantModule = defineModule({
144
204
  label: "Online token exchange",
145
205
  description: "Exchange admin session context for online app access.",
146
206
  remoteSafe: true,
147
- actions: [{ action: "exchange", wildcard: "explicit" }],
207
+ actions: [
208
+ {
209
+ action: "exchange",
210
+ label: "Exchange online token",
211
+ description: "Exchange admin session context for online app access.",
212
+ wildcard: "explicit",
213
+ },
214
+ ],
148
215
  },
149
216
  {
150
217
  id: "@voyant-travel/apps#access.finance-documents",
@@ -152,7 +219,13 @@ export const appsVoyantModule = defineModule({
152
219
  label: "Finance documents",
153
220
  description: "Read finance documents visible to remote accounting apps.",
154
221
  remoteSafe: true,
155
- actions: ["read"],
222
+ actions: [
223
+ {
224
+ action: "read",
225
+ label: "Read finance documents",
226
+ description: "Read finance documents visible to the app.",
227
+ },
228
+ ],
156
229
  wildcard: "explicit-resource",
157
230
  },
158
231
  {
@@ -162,9 +235,27 @@ export const appsVoyantModule = defineModule({
162
235
  description: "Request approved finance document actions.",
163
236
  remoteSafe: true,
164
237
  actions: [
165
- { action: "issue", sensitive: true, wildcard: "explicit" },
166
- { action: "retry", sensitive: true, wildcard: "explicit" },
167
- { action: "reconcile", sensitive: true, wildcard: "explicit" },
238
+ {
239
+ action: "issue",
240
+ label: "Issue finance documents",
241
+ description: "Request approved finance document issuance.",
242
+ sensitive: true,
243
+ wildcard: "explicit",
244
+ },
245
+ {
246
+ action: "retry",
247
+ label: "Retry finance actions",
248
+ description: "Request approved finance action retry.",
249
+ sensitive: true,
250
+ wildcard: "explicit",
251
+ },
252
+ {
253
+ action: "reconcile",
254
+ label: "Reconcile finance documents",
255
+ description: "Request approved finance document reconciliation.",
256
+ sensitive: true,
257
+ wildcard: "explicit",
258
+ },
168
259
  ],
169
260
  wildcard: "explicit-resource",
170
261
  },
@@ -189,6 +280,49 @@ export const appsVoyantModule = defineModule({
189
280
  },
190
281
  ],
191
282
  },
283
+ admin: {
284
+ compositionOrder: 170,
285
+ runtime: appsAdminRuntime,
286
+ copy: [
287
+ {
288
+ id: "@voyant-travel/apps#admin.copy",
289
+ namespace: "apps.admin",
290
+ fallbackLocale: "en",
291
+ runtime: {
292
+ entry: "@voyant-travel/apps-react/i18n",
293
+ export: "appsUiMessageDefinitions",
294
+ },
295
+ },
296
+ ],
297
+ routes: [
298
+ {
299
+ id: "@voyant-travel/apps#admin.route.installed",
300
+ path: "/apps",
301
+ requiredScopes: ["apps:read"],
302
+ runtime: appsAdminRuntime,
303
+ },
304
+ {
305
+ id: "@voyant-travel/apps#admin.route.developer",
306
+ path: "/apps/developer",
307
+ requiredScopes: ["apps:write"],
308
+ runtime: appsAdminRuntime,
309
+ },
310
+ ],
311
+ nav: [
312
+ {
313
+ id: "@voyant-travel/apps#admin.nav.installed",
314
+ routeId: "@voyant-travel/apps#admin.route.installed",
315
+ label: { namespace: "apps.admin", key: "navigation.title" },
316
+ order: 170,
317
+ },
318
+ {
319
+ id: "@voyant-travel/apps#admin.nav.developer",
320
+ routeId: "@voyant-travel/apps#admin.route.developer",
321
+ label: { namespace: "apps.admin", key: "navigation.developerTitle" },
322
+ order: 171,
323
+ },
324
+ ],
325
+ },
192
326
  lifecycle: { uninstall: { default: "retain-data", purge: "not-supported" } },
193
327
  meta: {
194
328
  ownership: "package",