@voyant-travel/apps 0.3.0 → 0.4.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 +15 -0
- package/dist/api-runtime.js +15 -1
- package/dist/app-api-contracts.d.ts +51 -0
- package/dist/app-api-contracts.js +50 -0
- package/dist/app-api-routes.d.ts +28 -0
- package/dist/app-api-routes.js +141 -0
- package/dist/app-api-service.d.ts +1263 -0
- package/dist/app-api-service.js +244 -0
- package/dist/app-api-service.test.d.ts +1 -0
- package/dist/app-api-service.test.js +109 -0
- package/dist/consent.js +16 -1
- package/dist/consent.test.js +24 -0
- package/dist/contracts.d.ts +3 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/installation-service.d.ts +1 -1
- package/dist/voyant.js +87 -1
- package/package.json +20 -5
package/dist/api-runtime.d.ts
CHANGED
|
@@ -7,4 +7,19 @@ export declare const createAppsApiModule: import("@voyant-travel/core/project").
|
|
|
7
7
|
db: import("drizzle-orm/postgres-js").PostgresJsDatabase;
|
|
8
8
|
};
|
|
9
9
|
}, import("hono/types").BlankSchema, "/">;
|
|
10
|
+
lazyRoutes: {
|
|
11
|
+
paths: string[];
|
|
12
|
+
load: () => Promise<import("hono").Hono<{
|
|
13
|
+
Variables: {
|
|
14
|
+
db: import("drizzle-orm/postgres-js").PostgresJsDatabase;
|
|
15
|
+
appId?: string;
|
|
16
|
+
appInstallationId?: string;
|
|
17
|
+
appReleaseId?: string;
|
|
18
|
+
appTokenMode?: "offline" | "online";
|
|
19
|
+
appViewerId?: string;
|
|
20
|
+
callerType?: string;
|
|
21
|
+
scopes?: string[];
|
|
22
|
+
};
|
|
23
|
+
}, import("hono/types").BlankSchema, "/">>;
|
|
24
|
+
};
|
|
10
25
|
}>;
|
package/dist/api-runtime.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { defineGraphRuntimeFactory } from "@voyant-travel/core/project";
|
|
2
|
+
import { customFieldValueLifecycleRuntimePort, customFieldValueOperationsRuntimePort, } from "@voyant-travel/core/runtime-port";
|
|
3
|
+
import { createCustomFieldTargetRegistry } from "@voyant-travel/custom-fields";
|
|
4
|
+
import { createAppsAppApiRoutes } from "./app-api-routes.js";
|
|
2
5
|
import { createAppsAdminRoutes } from "./routes.js";
|
|
3
|
-
export const createAppsApiModule = defineGraphRuntimeFactory(async ({ graph }) => {
|
|
6
|
+
export const createAppsApiModule = defineGraphRuntimeFactory(async ({ getPorts, graph }) => {
|
|
7
|
+
const customFieldTargets = createCustomFieldTargetRegistry(graph.customFieldTargets ?? []);
|
|
8
|
+
const customFieldValueLifecycles = await getPorts(customFieldValueLifecycleRuntimePort);
|
|
9
|
+
const customFieldValueOperations = await getPorts(customFieldValueOperationsRuntimePort);
|
|
4
10
|
return {
|
|
5
11
|
module: { name: "apps" },
|
|
6
12
|
adminRoutes: createAppsAdminRoutes({ eventCatalog: graph.eventCatalog }),
|
|
13
|
+
lazyRoutes: {
|
|
14
|
+
paths: ["/v1/app", "/v1/app/*"],
|
|
15
|
+
load: async () => createAppsAppApiRoutes({
|
|
16
|
+
customFieldTargets,
|
|
17
|
+
customFieldValueLifecycles,
|
|
18
|
+
customFieldValueOperations,
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
7
21
|
};
|
|
8
22
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { customFieldDefinitionInputSchema, customFieldDefinitionListQuerySchema, customFieldValueListQuerySchema, updateCustomFieldDefinitionSchema, upsertCustomFieldValueSchema } from "@voyant-travel/custom-fields";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export declare const APP_API_VERSION = "2026-07-01";
|
|
4
|
+
export declare const appApiVersionHeader = "voyant-app-api-version";
|
|
5
|
+
export declare const appApiEntityReadQuerySchema: z.ZodObject<{
|
|
6
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
7
|
+
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export declare const appApiFinanceDocumentQuerySchema: z.ZodObject<{
|
|
10
|
+
invoiceId: z.ZodOptional<z.ZodString>;
|
|
11
|
+
provider: z.ZodOptional<z.ZodString>;
|
|
12
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
13
|
+
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export declare const appApiFinanceActionSchema: z.ZodObject<{
|
|
16
|
+
action: z.ZodEnum<{
|
|
17
|
+
issue: "issue";
|
|
18
|
+
retry: "retry";
|
|
19
|
+
reconcile: "reconcile";
|
|
20
|
+
}>;
|
|
21
|
+
invoiceId: z.ZodOptional<z.ZodString>;
|
|
22
|
+
bookingId: z.ZodOptional<z.ZodString>;
|
|
23
|
+
approvalId: z.ZodOptional<z.ZodString>;
|
|
24
|
+
idempotencyKey: z.ZodString;
|
|
25
|
+
payload: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
26
|
+
}, z.core.$strict>;
|
|
27
|
+
export declare const appApiWebhookReplaySchema: z.ZodObject<{
|
|
28
|
+
deliveryId: z.ZodString;
|
|
29
|
+
signingKeyId: z.ZodString;
|
|
30
|
+
signingSecret: z.ZodString;
|
|
31
|
+
}, z.core.$strict>;
|
|
32
|
+
export declare const appApiAuditQuerySchema: z.ZodObject<{
|
|
33
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
34
|
+
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
export declare const appApiTokenExchangeSchema: z.ZodObject<{
|
|
37
|
+
client_id: z.ZodString;
|
|
38
|
+
client_secret: z.ZodOptional<z.ZodString>;
|
|
39
|
+
viewer_id: z.ZodString;
|
|
40
|
+
viewer_scopes: z.ZodArray<z.ZodString>;
|
|
41
|
+
contextual_scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
42
|
+
}, z.core.$strict>;
|
|
43
|
+
export declare const appApiDefinitionNamespaceSchema: z.ZodObject<{
|
|
44
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
export { customFieldDefinitionInputSchema as appApiCustomFieldDefinitionCreateSchema, customFieldDefinitionListQuerySchema as appApiCustomFieldDefinitionListQuerySchema, customFieldValueListQuerySchema as appApiCustomFieldValueListQuerySchema, updateCustomFieldDefinitionSchema as appApiCustomFieldDefinitionUpdateSchema, upsertCustomFieldValueSchema as appApiCustomFieldValueUpsertSchema, };
|
|
47
|
+
export type AppApiEntityReadQuery = z.infer<typeof appApiEntityReadQuerySchema>;
|
|
48
|
+
export type AppApiFinanceDocumentQuery = z.infer<typeof appApiFinanceDocumentQuerySchema>;
|
|
49
|
+
export type AppApiFinanceActionInput = z.infer<typeof appApiFinanceActionSchema>;
|
|
50
|
+
export type AppApiWebhookReplayInput = z.infer<typeof appApiWebhookReplaySchema>;
|
|
51
|
+
export type AppApiAuditQuery = z.infer<typeof appApiAuditQuerySchema>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { customFieldDefinitionInputSchema, customFieldDefinitionListQuerySchema, customFieldValueListQuerySchema, updateCustomFieldDefinitionSchema, upsertCustomFieldValueSchema, } from "@voyant-travel/custom-fields";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const APP_API_VERSION = "2026-07-01";
|
|
4
|
+
export const appApiVersionHeader = "voyant-app-api-version";
|
|
5
|
+
export const appApiEntityReadQuerySchema = z.object({
|
|
6
|
+
limit: z.coerce.number().int().min(1).max(100).default(25),
|
|
7
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
8
|
+
});
|
|
9
|
+
export const appApiFinanceDocumentQuerySchema = z.object({
|
|
10
|
+
invoiceId: z.string().min(1).optional(),
|
|
11
|
+
provider: z.string().min(1).optional(),
|
|
12
|
+
limit: z.coerce.number().int().min(1).max(100).default(25),
|
|
13
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
14
|
+
});
|
|
15
|
+
export const appApiFinanceActionSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
action: z.enum(["issue", "retry", "reconcile"]),
|
|
18
|
+
invoiceId: z.string().min(1).optional(),
|
|
19
|
+
bookingId: z.string().min(1).optional(),
|
|
20
|
+
approvalId: z.string().min(1).optional(),
|
|
21
|
+
idempotencyKey: z.string().min(1),
|
|
22
|
+
payload: z.record(z.string(), z.unknown()).default({}),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
export const appApiWebhookReplaySchema = z
|
|
26
|
+
.object({
|
|
27
|
+
deliveryId: z.string().min(1),
|
|
28
|
+
signingKeyId: z.string().min(1),
|
|
29
|
+
signingSecret: z.string().min(1),
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
32
|
+
export const appApiAuditQuerySchema = z.object({
|
|
33
|
+
limit: z.coerce.number().int().min(1).max(100).default(25),
|
|
34
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
35
|
+
});
|
|
36
|
+
export const appApiTokenExchangeSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
client_id: z.string().min(1),
|
|
39
|
+
client_secret: z.string().optional(),
|
|
40
|
+
viewer_id: z.string().min(1),
|
|
41
|
+
viewer_scopes: z.array(z.string().min(1)),
|
|
42
|
+
contextual_scopes: z.array(z.string().min(1)).optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
export const appApiDefinitionNamespaceSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
namespace: z.string().optional(),
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
export { customFieldDefinitionInputSchema as appApiCustomFieldDefinitionCreateSchema, customFieldDefinitionListQuerySchema as appApiCustomFieldDefinitionListQuerySchema, customFieldValueListQuerySchema as appApiCustomFieldValueListQuerySchema, updateCustomFieldDefinitionSchema as appApiCustomFieldDefinitionUpdateSchema, upsertCustomFieldValueSchema as appApiCustomFieldValueUpsertSchema, };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AccessCatalog } from "@voyant-travel/types/api-keys";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { type AppApiServiceOptions } from "./app-api-service.js";
|
|
5
|
+
type Env = {
|
|
6
|
+
Variables: {
|
|
7
|
+
db: PostgresJsDatabase;
|
|
8
|
+
appId?: string;
|
|
9
|
+
appInstallationId?: string;
|
|
10
|
+
appReleaseId?: string;
|
|
11
|
+
appTokenMode?: "offline" | "online";
|
|
12
|
+
appViewerId?: string;
|
|
13
|
+
callerType?: string;
|
|
14
|
+
scopes?: string[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** Absolute mount prefix for the versioned App API surface. */
|
|
18
|
+
export declare const APP_API_BASE_PATH = "/v1/app";
|
|
19
|
+
export interface AppsAppApiRouteOptions extends AppApiServiceOptions {
|
|
20
|
+
oauth?: {
|
|
21
|
+
accessCatalog: AccessCatalog;
|
|
22
|
+
deploymentId: string;
|
|
23
|
+
};
|
|
24
|
+
deadlineMs?: number;
|
|
25
|
+
maxPayloadBytes?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function createAppsAppApiRoutes(options?: AppsAppApiRouteOptions): Hono<Env, import("hono/types").BlankSchema, "/">;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ApiHttpError, parseJsonBody, parseQuery, RequestValidationError, } from "@voyant-travel/hono";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { appApiAuditQuerySchema, appApiCustomFieldDefinitionCreateSchema, appApiCustomFieldDefinitionListQuerySchema, appApiCustomFieldDefinitionUpdateSchema, appApiCustomFieldValueListQuerySchema, appApiCustomFieldValueUpsertSchema, appApiEntityReadQuerySchema, appApiFinanceActionSchema, appApiFinanceDocumentQuerySchema, appApiTokenExchangeSchema, appApiVersionHeader, appApiWebhookReplaySchema, } from "./app-api-contracts.js";
|
|
5
|
+
import { createAppApiService, withAppApiDeadline, } from "./app-api-service.js";
|
|
6
|
+
import { createAppOAuthService } from "./oauth-service.js";
|
|
7
|
+
import { replayAppWebhookDelivery } from "./webhook-delivery.js";
|
|
8
|
+
/** Absolute mount prefix for the versioned App API surface. */
|
|
9
|
+
export const APP_API_BASE_PATH = "/v1/app";
|
|
10
|
+
const idParamSchema = z.object({ id: z.string().min(1) });
|
|
11
|
+
const entityParamSchema = z.object({ entity: z.string().min(1) });
|
|
12
|
+
const definitionBodySchema = z
|
|
13
|
+
.object({
|
|
14
|
+
namespace: z.string().optional(),
|
|
15
|
+
definition: appApiCustomFieldDefinitionCreateSchema,
|
|
16
|
+
})
|
|
17
|
+
.strict();
|
|
18
|
+
const definitionUpdateBodySchema = z
|
|
19
|
+
.object({
|
|
20
|
+
namespace: z.string().optional(),
|
|
21
|
+
definition: appApiCustomFieldDefinitionUpdateSchema,
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
export function createAppsAppApiRoutes(options = {}) {
|
|
25
|
+
const routes = new Hono();
|
|
26
|
+
const service = createAppApiService(options);
|
|
27
|
+
const oauth = options.oauth ? createAppOAuthService(options.oauth) : null;
|
|
28
|
+
const maxPayloadBytes = options.maxPayloadBytes ?? 256 * 1024;
|
|
29
|
+
routes.use("*", async (c, next) => {
|
|
30
|
+
if (c.get("callerType") !== "app") {
|
|
31
|
+
throw new ApiHttpError("App API requires an app access token.", {
|
|
32
|
+
status: 401,
|
|
33
|
+
code: "app_api_token_required",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const length = Number(c.req.header("content-length") ?? "0");
|
|
37
|
+
if (Number.isFinite(length) && length > maxPayloadBytes) {
|
|
38
|
+
throw new ApiHttpError("App API payload is too large.", {
|
|
39
|
+
status: 413,
|
|
40
|
+
code: "app_api_payload_too_large",
|
|
41
|
+
details: { maxPayloadBytes },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return next();
|
|
45
|
+
});
|
|
46
|
+
routes.get("/self", (c) => run(c, service.introspect(c.get("db"), appContext(c)), options.deadlineMs));
|
|
47
|
+
routes.get("/entities/:entity", (c) => {
|
|
48
|
+
const { entity } = parseEntityParams(c.req.param());
|
|
49
|
+
const query = parseQuery(c, appApiEntityReadQuerySchema);
|
|
50
|
+
return run(c, service.listEntities(c.get("db"), appContext(c), entity, query), options.deadlineMs);
|
|
51
|
+
});
|
|
52
|
+
routes.get("/finance/documents", (c) => run(c, service.listFinanceDocuments(c.get("db"), appContext(c), parseQuery(c, appApiFinanceDocumentQuerySchema)), options.deadlineMs));
|
|
53
|
+
routes.post("/finance/actions", async (c) => {
|
|
54
|
+
const body = await parseJsonBody(c, appApiFinanceActionSchema);
|
|
55
|
+
return run(c, service.executeFinanceAction(c.get("db"), appContext(c), body), options.deadlineMs);
|
|
56
|
+
});
|
|
57
|
+
routes.get("/custom-fields/definitions", (c) => run(c, service.listCustomFieldDefinitions(c.get("db"), appContext(c), parseQuery(c, appApiCustomFieldDefinitionListQuerySchema)), options.deadlineMs));
|
|
58
|
+
routes.post("/custom-fields/definitions", async (c) => {
|
|
59
|
+
const body = await parseJsonBody(c, definitionBodySchema);
|
|
60
|
+
return run(c, service.createCustomFieldDefinition(c.get("db"), appContext(c), body.namespace, body.definition), options.deadlineMs);
|
|
61
|
+
});
|
|
62
|
+
routes.patch("/custom-fields/definitions/:id", async (c) => {
|
|
63
|
+
const { id } = parseIdParams(c.req.param());
|
|
64
|
+
const body = await parseJsonBody(c, definitionUpdateBodySchema);
|
|
65
|
+
return run(c, service.updateCustomFieldDefinition(c.get("db"), appContext(c), id, body.namespace, body.definition), options.deadlineMs);
|
|
66
|
+
});
|
|
67
|
+
routes.get("/custom-fields/values", (c) => run(c, service.listCustomFieldValues(c.get("db"), appContext(c), parseQuery(c, appApiCustomFieldValueListQuerySchema)), options.deadlineMs));
|
|
68
|
+
routes.put("/custom-fields/definitions/:id/value", async (c) => {
|
|
69
|
+
const { id } = parseIdParams(c.req.param());
|
|
70
|
+
const body = await parseJsonBody(c, appApiCustomFieldValueUpsertSchema);
|
|
71
|
+
return run(c, service.upsertCustomFieldValue(c.get("db"), appContext(c), id, body), options.deadlineMs);
|
|
72
|
+
});
|
|
73
|
+
routes.get("/webhooks", (c) => run(c, service.listWebhookHealth(c.get("db"), appContext(c)), options.deadlineMs));
|
|
74
|
+
routes.post("/webhooks/replay", async (c) => {
|
|
75
|
+
await service.requireAccess(c.get("db"), appContext(c), ["webhooks:replay"]);
|
|
76
|
+
const body = await parseJsonBody(c, appApiWebhookReplaySchema);
|
|
77
|
+
return run(c, replayAppWebhookDelivery(c.get("db"), {
|
|
78
|
+
deliveryId: body.deliveryId,
|
|
79
|
+
actorId: appContext(c).appId,
|
|
80
|
+
signingKey: { id: body.signingKeyId, secret: body.signingSecret },
|
|
81
|
+
}), options.deadlineMs, 202);
|
|
82
|
+
});
|
|
83
|
+
routes.get("/audit", (c) => run(c, service.listAuditHistory(c.get("db"), appContext(c), parseQuery(c, appApiAuditQuerySchema)), options.deadlineMs));
|
|
84
|
+
routes.post("/oauth/token-exchange", async (c) => {
|
|
85
|
+
if (!oauth)
|
|
86
|
+
return c.json({ error: "App OAuth is not configured" }, 501);
|
|
87
|
+
const context = appContext(c);
|
|
88
|
+
await service.requireAccess(c.get("db"), context, ["online-token:exchange"]);
|
|
89
|
+
const body = await parseJsonBody(c, appApiTokenExchangeSchema);
|
|
90
|
+
return run(c, oauth.token(c.get("db"), {
|
|
91
|
+
grantType: "urn:voyant:params:oauth:grant-type:actor-token-exchange",
|
|
92
|
+
installationId: context.installationId,
|
|
93
|
+
viewerId: body.viewer_id,
|
|
94
|
+
viewerScopes: body.viewer_scopes,
|
|
95
|
+
contextualScopes: body.contextual_scopes,
|
|
96
|
+
clientId: body.client_id,
|
|
97
|
+
clientSecret: body.client_secret,
|
|
98
|
+
}), options.deadlineMs);
|
|
99
|
+
});
|
|
100
|
+
// Lazy route families forward the original absolute URL, so the loaded
|
|
101
|
+
// sub-app must expose absolute paths. Mount the relative handlers under the
|
|
102
|
+
// App API prefix; the matchers in api-runtime install `/v1/app` and
|
|
103
|
+
// `/v1/app/*` up front.
|
|
104
|
+
const app = new Hono();
|
|
105
|
+
app.route(APP_API_BASE_PATH, routes);
|
|
106
|
+
return app;
|
|
107
|
+
}
|
|
108
|
+
function appContext(c) {
|
|
109
|
+
const appId = c.get("appId");
|
|
110
|
+
const installationId = c.get("appInstallationId");
|
|
111
|
+
const releaseId = c.get("appReleaseId");
|
|
112
|
+
const tokenMode = c.get("appTokenMode");
|
|
113
|
+
if (!appId || !installationId || !releaseId || !tokenMode) {
|
|
114
|
+
throw new RequestValidationError("App token context is missing.");
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
appId,
|
|
118
|
+
installationId,
|
|
119
|
+
releaseId,
|
|
120
|
+
tokenMode,
|
|
121
|
+
viewerId: c.get("appViewerId"),
|
|
122
|
+
scopes: c.get("scopes") ?? [],
|
|
123
|
+
apiVersion: c.req.header(appApiVersionHeader) ?? undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseIdParams(input) {
|
|
127
|
+
const parsed = idParamSchema.safeParse(input);
|
|
128
|
+
if (!parsed.success)
|
|
129
|
+
throw new RequestValidationError("Invalid route parameters");
|
|
130
|
+
return parsed.data;
|
|
131
|
+
}
|
|
132
|
+
function parseEntityParams(input) {
|
|
133
|
+
const parsed = entityParamSchema.safeParse(input);
|
|
134
|
+
if (!parsed.success)
|
|
135
|
+
throw new RequestValidationError("Invalid route parameters");
|
|
136
|
+
return parsed.data;
|
|
137
|
+
}
|
|
138
|
+
async function run(c, promise, deadlineMs, status = 200) {
|
|
139
|
+
const data = await withAppApiDeadline(promise, deadlineMs);
|
|
140
|
+
return c.json(data, status);
|
|
141
|
+
}
|