@voyant-travel/apps 0.7.0 → 0.8.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.
Files changed (38) hide show
  1. package/dist/api-runtime.d.ts +23 -11
  2. package/dist/api-runtime.js +38 -1
  3. package/dist/api-runtime.test.d.ts +1 -0
  4. package/dist/api-runtime.test.js +54 -0
  5. package/dist/app-api-contracts.d.ts +58 -8
  6. package/dist/app-api-contracts.js +102 -9
  7. package/dist/app-api-finance-routes.test.js +482 -2
  8. package/dist/app-api-routes.d.ts +6 -5
  9. package/dist/app-api-routes.js +131 -22
  10. package/dist/app-api-service.d.ts +31 -1
  11. package/dist/app-api-service.js +162 -0
  12. package/dist/consent.d.ts +1 -0
  13. package/dist/consent.js +2 -2
  14. package/dist/contracts.d.ts +1 -25
  15. package/dist/contracts.js +3 -10
  16. package/dist/oauth-service.d.ts +6 -1
  17. package/dist/oauth-service.js +41 -4
  18. package/dist/oauth-service.test.js +39 -2
  19. package/dist/routes-openapi.d.ts +1 -25
  20. package/dist/routes-viewer-scopes.test.d.ts +1 -0
  21. package/dist/routes-viewer-scopes.test.js +46 -0
  22. package/dist/routes.d.ts +5 -5
  23. package/dist/routes.js +41 -24
  24. package/dist/runtime-contributor.d.ts +9 -1
  25. package/dist/runtime-contributor.js +25 -2
  26. package/dist/runtime-contributor.test.d.ts +1 -0
  27. package/dist/runtime-contributor.test.js +52 -0
  28. package/dist/runtime-port.d.ts +8 -0
  29. package/dist/runtime-port.js +22 -0
  30. package/dist/session-token-service.d.ts +2 -0
  31. package/dist/session-token-service.js +44 -27
  32. package/dist/session-token-service.test.d.ts +1 -0
  33. package/dist/session-token-service.test.js +157 -0
  34. package/dist/session-token.d.ts +5 -1
  35. package/dist/session-token.js +0 -0
  36. package/dist/session-token.test.js +5 -0
  37. package/dist/voyant.js +95 -1
  38. package/package.json +8 -3
@@ -1,18 +1,10 @@
1
1
  export declare const createAppsApiModule: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<{
2
- module: {
3
- name: string;
4
- };
5
- adminRoutes: import("@hono/zod-openapi").OpenAPIHono<{
6
- Bindings: {
7
- VOYANT_CLOUD_DEPLOYMENT_ID?: string;
8
- };
9
- Variables: {
10
- db: import("drizzle-orm/postgres-js").PostgresJsDatabase;
11
- };
12
- }, {}, "/">;
13
2
  lazyRoutes: {
14
3
  paths: string[];
15
4
  load: () => Promise<import("hono").Hono<{
5
+ Bindings: {
6
+ API_BASE_URL?: string;
7
+ };
16
8
  Variables: {
17
9
  db: import("drizzle-orm/postgres-js").PostgresJsDatabase;
18
10
  appId?: string;
@@ -20,9 +12,29 @@ export declare const createAppsApiModule: import("@voyant-travel/core/project").
20
12
  appReleaseId?: string;
21
13
  appTokenMode?: "offline" | "online";
22
14
  appViewerId?: string;
15
+ appContextConstraint?: import("@voyant-travel/core").VoyantAppContextConstraint;
23
16
  callerType?: string;
24
17
  scopes?: string[];
25
18
  };
26
19
  }, import("hono/types").BlankSchema, "/">>;
27
20
  };
21
+ clientAuthenticated?: readonly [{
22
+ readonly method: "POST";
23
+ readonly path: "/oauth/token";
24
+ }, {
25
+ readonly method: "POST";
26
+ readonly path: "/oauth/session-token/exchange";
27
+ }] | undefined;
28
+ authAugmentation?: {
29
+ resolveAppToken: ({ db, token }: import("@voyant-travel/hono").VoyantAuthAppTokenResolveArgs<import("@voyant-travel/hono").VoyantBindings>) => Promise<import("@voyant-travel/core").VoyantAuthContext | null>;
30
+ } | undefined;
31
+ module: {
32
+ name: string;
33
+ };
34
+ adminRoutes: import("@hono/zod-openapi").OpenAPIHono<{
35
+ Variables: {
36
+ db: import("drizzle-orm/postgres-js").PostgresJsDatabase;
37
+ scopes?: string[];
38
+ };
39
+ }, {}, "/">;
28
40
  }>;
@@ -3,7 +3,9 @@ import { customFieldValueLifecycleRuntimePort, customFieldValueOperationsRuntime
3
3
  import { createCustomFieldTargetRegistry } from "@voyant-travel/custom-fields";
4
4
  import { financeAppApiRuntimePort } from "@voyant-travel/finance-contracts/app-api";
5
5
  import { createAppsAppApiRoutes } from "./app-api-routes.js";
6
+ import { createAppOAuthService } from "./oauth-service.js";
6
7
  import { createAppsAdminRoutes } from "./routes.js";
8
+ import { appsManagedAuthRuntimePort } from "./runtime-port.js";
7
9
  export const createAppsApiModule = defineGraphRuntimeFactory(async ({ getPort, getPorts, graph, hasPort }) => {
8
10
  const customFieldTargets = createCustomFieldTargetRegistry(graph.customFieldTargets ?? []);
9
11
  const customFieldValueLifecycles = await getPorts(customFieldValueLifecycleRuntimePort);
@@ -11,9 +13,44 @@ export const createAppsApiModule = defineGraphRuntimeFactory(async ({ getPort, g
11
13
  const finance = hasPort(financeAppApiRuntimePort)
12
14
  ? await getPort(financeAppApiRuntimePort)
13
15
  : undefined;
16
+ const managedAuth = hasPort(appsManagedAuthRuntimePort)
17
+ ? await getPort(appsManagedAuthRuntimePort)
18
+ : undefined;
19
+ const oauthOptions = managedAuth
20
+ ? {
21
+ accessCatalog: graph.accessCatalog,
22
+ deploymentId: managedAuth.runtimeAudience,
23
+ clientAuthentication: "required",
24
+ }
25
+ : undefined;
26
+ const oauth = oauthOptions ? createAppOAuthService(oauthOptions) : undefined;
14
27
  return {
15
28
  module: { name: "apps" },
16
- adminRoutes: createAppsAdminRoutes({ eventCatalog: graph.eventCatalog }),
29
+ adminRoutes: createAppsAdminRoutes({
30
+ eventCatalog: graph.eventCatalog,
31
+ ...(oauthOptions ? { oauth: oauthOptions } : {}),
32
+ ...(managedAuth
33
+ ? {
34
+ sessionToken: {
35
+ secret: managedAuth.sessionTokenSigningSecret,
36
+ ...(managedAuth.sessionTokenTtlSeconds === undefined
37
+ ? {}
38
+ : { ttlSeconds: managedAuth.sessionTokenTtlSeconds }),
39
+ },
40
+ }
41
+ : {}),
42
+ }),
43
+ ...(oauth
44
+ ? {
45
+ clientAuthenticated: [
46
+ { method: "POST", path: "/oauth/token" },
47
+ { method: "POST", path: "/oauth/session-token/exchange" },
48
+ ],
49
+ authAugmentation: {
50
+ resolveAppToken: ({ db, token }) => oauth.resolveAccessToken(db, token),
51
+ },
52
+ }
53
+ : {}),
17
54
  lazyRoutes: {
18
55
  paths: ["/v1/app", "/v1/app/*"],
19
56
  load: async () => createAppsAppApiRoutes({
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createAppsApiModule } from "./api-runtime.js";
3
+ import { appsManagedAuthRuntimePort } from "./runtime-port.js";
4
+ const graph = {
5
+ providerSelections: {},
6
+ customFieldTargets: [],
7
+ accessCatalog: { resources: [], presets: [] },
8
+ eventCatalog: { schemaVersion: "voyant.event-catalog.v1", events: [] },
9
+ tools: [],
10
+ references: [],
11
+ setupSteps: [],
12
+ };
13
+ function context(managedAuth) {
14
+ return {
15
+ unitId: "@voyant-travel/apps",
16
+ projectConfig: {},
17
+ getUnitProjectConfig: () => undefined,
18
+ api: [],
19
+ graph,
20
+ runtimePorts: managedAuth ? { [appsManagedAuthRuntimePort.id]: managedAuth } : {},
21
+ hasPort: (port) => port.id === appsManagedAuthRuntimePort.id && managedAuth !== undefined,
22
+ getPort: async (port) => {
23
+ if (port.id === appsManagedAuthRuntimePort.id && managedAuth)
24
+ return managedAuth;
25
+ throw new Error(`missing ${port.id}`);
26
+ },
27
+ getPorts: async () => [],
28
+ };
29
+ }
30
+ describe("createAppsApiModule", () => {
31
+ it("keeps managed auth and client-authenticated routes off without host inputs", async () => {
32
+ const module = await createAppsApiModule(context());
33
+ expect(module.clientAuthenticated).toBeUndefined();
34
+ expect(module.authAugmentation).toBeUndefined();
35
+ });
36
+ it("composes OAuth, session exchange, and token resolution from the host port", async () => {
37
+ const module = await createAppsApiModule(context({
38
+ runtimeAudience: "deployment-1",
39
+ sessionTokenSigningSecret: "s".repeat(32),
40
+ sessionTokenTtlSeconds: 180,
41
+ }));
42
+ expect(module.clientAuthenticated).toEqual([
43
+ { method: "POST", path: "/oauth/token" },
44
+ { method: "POST", path: "/oauth/session-token/exchange" },
45
+ ]);
46
+ expect(module.authAugmentation?.resolveAppToken).toBeTypeOf("function");
47
+ await expect(module.authAugmentation?.resolveAppToken({
48
+ request: new Request("http://test/v1/app"),
49
+ env: { DATABASE_URL: "postgres://test" },
50
+ db: {},
51
+ token: "test-token",
52
+ })).resolves.toBeNull();
53
+ });
54
+ });
@@ -1,5 +1,5 @@
1
1
  import { customFieldDefinitionInputSchema, customFieldDefinitionListQuerySchema, customFieldValueListQuerySchema, updateCustomFieldDefinitionSchema, upsertCustomFieldValueSchema } from "@voyant-travel/custom-fields";
2
- import type { FinanceAppApiExternalReference, FinanceAppApiIssuanceDocument } from "@voyant-travel/finance-contracts/app-api";
2
+ import type { FinanceAppApiExternalLifecycleObservation, FinanceAppApiExternalReference, FinanceAppApiExternalSyncState, FinanceAppApiIssuanceDocument, FinanceAppApiPdfArtifact, FinanceAppApiSettlementObservation } from "@voyant-travel/finance-contracts/app-api";
3
3
  import { z } from "zod";
4
4
  export declare const APP_API_VERSION = "2026-07-01";
5
5
  export declare const appApiVersionHeader = "voyant-app-api-version";
@@ -48,6 +48,53 @@ export declare const appApiFinanceExternalReferenceUpsertSchema: z.ZodObject<{
48
48
  invoiceNumber: z.ZodString;
49
49
  }, z.core.$strict>>;
50
50
  }, z.core.$strict>;
51
+ export declare const appApiFinancePdfArtifactHeadersSchema: z.ZodObject<{
52
+ idempotencyKey: z.ZodString;
53
+ fileName: z.ZodString;
54
+ }, z.core.$strict>;
55
+ export declare const appApiFinanceExternalSyncStateSchema: z.ZodObject<{
56
+ operationId: z.ZodString;
57
+ status: z.ZodEnum<{
58
+ succeeded: "succeeded";
59
+ retryable_failure: "retryable_failure";
60
+ terminal_failure: "terminal_failure";
61
+ }>;
62
+ occurredAt: z.ZodString;
63
+ error: z.ZodDefault<z.ZodNullable<z.ZodObject<{
64
+ code: z.ZodString;
65
+ message: z.ZodString;
66
+ }, z.core.$strict>>>;
67
+ metadata: z.ZodDefault<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
68
+ }, z.core.$strict>;
69
+ export declare const appApiFinanceExternalLifecycleStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
70
+ operationId: z.ZodString;
71
+ state: z.ZodLiteral<"converted">;
72
+ occurredAt: z.ZodString;
73
+ lineage: z.ZodObject<{
74
+ sourceDocumentId: z.ZodString;
75
+ successorDocumentId: z.ZodString;
76
+ }, z.core.$strict>;
77
+ }, z.core.$strict>, z.ZodObject<{
78
+ operationId: z.ZodString;
79
+ state: z.ZodLiteral<"voided">;
80
+ occurredAt: z.ZodString;
81
+ lineage: z.ZodDefault<z.ZodNull>;
82
+ }, z.core.$strict>], "state">;
83
+ export declare const appApiFinanceSettlementObservationSchema: z.ZodObject<{
84
+ operationId: z.ZodString;
85
+ occurredAt: z.ZodString;
86
+ status: z.ZodEnum<{
87
+ partial: "partial";
88
+ paid: "paid";
89
+ }>;
90
+ currency: z.ZodString;
91
+ totals: z.ZodObject<{
92
+ totalCents: z.ZodNumber;
93
+ paidCents: z.ZodNumber;
94
+ balanceDueCents: z.ZodNumber;
95
+ }, z.core.$strict>;
96
+ paymentIdentifiers: z.ZodArray<z.ZodString>;
97
+ }, z.core.$strict>;
51
98
  export declare const appApiWebhookReplaySchema: z.ZodObject<{
52
99
  deliveryId: z.ZodString;
53
100
  signingKeyId: z.ZodString;
@@ -57,13 +104,6 @@ export declare const appApiAuditQuerySchema: z.ZodObject<{
57
104
  limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
58
105
  offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
59
106
  }, z.core.$strip>;
60
- export declare const appApiTokenExchangeSchema: z.ZodObject<{
61
- client_id: z.ZodString;
62
- client_secret: z.ZodOptional<z.ZodString>;
63
- viewer_id: z.ZodString;
64
- viewer_scopes: z.ZodArray<z.ZodString>;
65
- contextual_scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
66
- }, z.core.$strict>;
67
107
  export declare const appApiDefinitionNamespaceSchema: z.ZodObject<{
68
108
  namespace: z.ZodOptional<z.ZodString>;
69
109
  }, z.core.$strict>;
@@ -72,7 +112,17 @@ export type AppApiEntityReadQuery = z.infer<typeof appApiEntityReadQuerySchema>;
72
112
  export type AppApiFinanceDocumentQuery = z.infer<typeof appApiFinanceDocumentQuerySchema>;
73
113
  export type AppApiFinanceActionInput = z.infer<typeof appApiFinanceActionSchema>;
74
114
  export type AppApiFinanceExternalReferenceUpsertInput = z.infer<typeof appApiFinanceExternalReferenceUpsertSchema>;
115
+ export type AppApiFinancePdfArtifactHeaders = z.infer<typeof appApiFinancePdfArtifactHeadersSchema>;
116
+ export type AppApiFinanceExternalSyncStateInput = z.infer<typeof appApiFinanceExternalSyncStateSchema>;
117
+ export type AppApiFinanceExternalLifecycleStateInput = z.infer<typeof appApiFinanceExternalLifecycleStateSchema>;
118
+ export type AppApiFinanceSettlementObservationInput = z.infer<typeof appApiFinanceSettlementObservationSchema>;
75
119
  export type AppApiWebhookReplayInput = z.infer<typeof appApiWebhookReplaySchema>;
76
120
  export type AppApiAuditQuery = z.infer<typeof appApiAuditQuerySchema>;
77
121
  export type AppApiFinanceIssuanceDocument = FinanceAppApiIssuanceDocument;
78
122
  export type AppApiFinanceExternalReference = FinanceAppApiExternalReference;
123
+ export type AppApiFinancePdfArtifact = FinanceAppApiPdfArtifact & {
124
+ documentUrl: string;
125
+ };
126
+ export type AppApiFinanceExternalSyncState = FinanceAppApiExternalSyncState;
127
+ export type AppApiFinanceExternalLifecycleObservation = FinanceAppApiExternalLifecycleObservation;
128
+ export type AppApiFinanceSettlementObservation = FinanceAppApiSettlementObservation;
@@ -50,6 +50,108 @@ export const appApiFinanceExternalReferenceUpsertSchema = z
50
50
  .optional(),
51
51
  })
52
52
  .strict();
53
+ export const appApiFinancePdfArtifactHeadersSchema = z
54
+ .object({
55
+ idempotencyKey: z.string().trim().min(1).max(200),
56
+ fileName: z
57
+ .string()
58
+ .trim()
59
+ .min(1)
60
+ .max(200)
61
+ .refine((value) => [...value].every((character) => {
62
+ const code = character.charCodeAt(0);
63
+ return code >= 32 && code !== 127 && character !== "/" && character !== "\\";
64
+ }), "Invalid artifact file name."),
65
+ })
66
+ .strict();
67
+ const appApiFinanceExternalSyncErrorSchema = z
68
+ .object({
69
+ code: z.string().trim().min(1).max(100),
70
+ message: z.string().trim().min(1).max(2_000),
71
+ })
72
+ .strict();
73
+ export const appApiFinanceExternalSyncStateSchema = z
74
+ .object({
75
+ operationId: z.string().trim().min(1).max(200),
76
+ status: z.enum(["succeeded", "retryable_failure", "terminal_failure"]),
77
+ occurredAt: z.string().datetime(),
78
+ error: appApiFinanceExternalSyncErrorSchema.nullable().default(null),
79
+ metadata: z
80
+ .record(z.string().trim().min(1).max(100), z.unknown())
81
+ .refine((value) => JSON.stringify(value).length <= 16_384, "Metadata is too large.")
82
+ .nullable()
83
+ .default(null),
84
+ })
85
+ .strict()
86
+ .superRefine((value, context) => {
87
+ if (value.status === "succeeded" && value.error) {
88
+ context.addIssue({
89
+ code: "custom",
90
+ message: "Successful sync state cannot include an error.",
91
+ });
92
+ }
93
+ if (value.status !== "succeeded" && !value.error) {
94
+ context.addIssue({ code: "custom", message: "Failed sync state requires an error." });
95
+ }
96
+ });
97
+ const appApiFinanceDocumentLineageSchema = z
98
+ .object({
99
+ sourceDocumentId: z.string().trim().min(1).max(200),
100
+ successorDocumentId: z.string().trim().min(1).max(200),
101
+ })
102
+ .strict();
103
+ export const appApiFinanceExternalLifecycleStateSchema = z.discriminatedUnion("state", [
104
+ z
105
+ .object({
106
+ operationId: z.string().trim().min(1).max(200),
107
+ state: z.literal("converted"),
108
+ occurredAt: z.string().datetime(),
109
+ lineage: appApiFinanceDocumentLineageSchema,
110
+ })
111
+ .strict(),
112
+ z
113
+ .object({
114
+ operationId: z.string().trim().min(1).max(200),
115
+ state: z.literal("voided"),
116
+ occurredAt: z.string().datetime(),
117
+ lineage: z.null().default(null),
118
+ })
119
+ .strict(),
120
+ ]);
121
+ export const appApiFinanceSettlementObservationSchema = z
122
+ .object({
123
+ operationId: z.string().trim().min(1).max(200),
124
+ occurredAt: z.string().datetime(),
125
+ status: z.enum(["partial", "paid"]),
126
+ currency: z
127
+ .string()
128
+ .trim()
129
+ .regex(/^[A-Z]{3}$/),
130
+ totals: z
131
+ .object({
132
+ totalCents: z.number().int().nonnegative(),
133
+ paidCents: z.number().int().positive(),
134
+ balanceDueCents: z.number().int().nonnegative(),
135
+ })
136
+ .strict(),
137
+ paymentIdentifiers: z
138
+ .array(z.string().trim().min(1).max(200))
139
+ .min(1)
140
+ .max(100)
141
+ .refine((value) => new Set(value).size === value.length, "Payment identifiers must be unique."),
142
+ })
143
+ .strict()
144
+ .superRefine((value, context) => {
145
+ if (value.totals.paidCents + value.totals.balanceDueCents !== value.totals.totalCents) {
146
+ context.addIssue({ code: "custom", message: "Settlement totals must balance." });
147
+ }
148
+ if (value.status === "partial" && value.totals.balanceDueCents === 0) {
149
+ context.addIssue({ code: "custom", message: "Partial settlement requires a balance." });
150
+ }
151
+ if (value.status === "paid" && value.totals.balanceDueCents !== 0) {
152
+ context.addIssue({ code: "custom", message: "Paid settlement cannot have a balance." });
153
+ }
154
+ });
53
155
  export const appApiWebhookReplaySchema = z
54
156
  .object({
55
157
  deliveryId: z.string().min(1),
@@ -61,15 +163,6 @@ export const appApiAuditQuerySchema = z.object({
61
163
  limit: z.coerce.number().int().min(1).max(100).default(25),
62
164
  offset: z.coerce.number().int().min(0).default(0),
63
165
  });
64
- export const appApiTokenExchangeSchema = z
65
- .object({
66
- client_id: z.string().min(1),
67
- client_secret: z.string().optional(),
68
- viewer_id: z.string().min(1),
69
- viewer_scopes: z.array(z.string().min(1)),
70
- contextual_scopes: z.array(z.string().min(1)).optional(),
71
- })
72
- .strict();
73
166
  export const appApiDefinitionNamespaceSchema = z
74
167
  .object({
75
168
  namespace: z.string().optional(),