@voyant-travel/apps 0.6.3 → 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.
- package/dist/api-runtime.d.ts +23 -11
- package/dist/api-runtime.js +44 -2
- package/dist/api-runtime.test.d.ts +1 -0
- package/dist/api-runtime.test.js +54 -0
- package/dist/app-api-contracts.d.ts +84 -7
- package/dist/app-api-contracts.js +130 -9
- package/dist/app-api-finance-routes.test.d.ts +1 -0
- package/dist/app-api-finance-routes.test.js +633 -0
- package/dist/app-api-routes.d.ts +6 -5
- package/dist/app-api-routes.js +144 -22
- package/dist/app-api-service.d.ts +49 -4
- package/dist/app-api-service.js +237 -0
- package/dist/app-api-service.test.js +52 -0
- package/dist/consent.d.ts +1 -0
- package/dist/consent.js +2 -2
- package/dist/contracts.d.ts +4 -28
- package/dist/contracts.js +3 -10
- package/dist/oauth-service.d.ts +6 -1
- package/dist/oauth-service.js +41 -4
- package/dist/oauth-service.test.js +39 -2
- package/dist/routes-openapi.d.ts +1 -25
- package/dist/routes-viewer-scopes.test.d.ts +1 -0
- package/dist/routes-viewer-scopes.test.js +46 -0
- package/dist/routes.d.ts +5 -5
- package/dist/routes.js +41 -24
- package/dist/runtime-contributor.d.ts +9 -1
- package/dist/runtime-contributor.js +25 -2
- package/dist/runtime-contributor.test.d.ts +1 -0
- package/dist/runtime-contributor.test.js +52 -0
- package/dist/runtime-port.d.ts +8 -0
- package/dist/runtime-port.js +22 -0
- package/dist/session-token-service.d.ts +2 -0
- package/dist/session-token-service.js +44 -27
- package/dist/session-token-service.test.d.ts +1 -0
- package/dist/session-token-service.test.js +157 -0
- package/dist/session-token.d.ts +5 -1
- package/dist/session-token.js +0 -0
- package/dist/session-token.test.js +5 -0
- package/dist/voyant.js +136 -1
- package/package.json +9 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface AppsManagedAuthRuntime {
|
|
2
|
+
/** Stable audience shared by authorization, installations, and session tokens. */
|
|
3
|
+
runtimeAudience: string;
|
|
4
|
+
/** Host-owned HMAC material for short-lived extension session tokens. */
|
|
5
|
+
sessionTokenSigningSecret: string;
|
|
6
|
+
sessionTokenTtlSeconds?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const appsManagedAuthRuntimePort: import("@voyant-travel/core/project").VoyantPort<AppsManagedAuthRuntime>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { definePort } from "@voyant-travel/core/project";
|
|
2
|
+
export const appsManagedAuthRuntimePort = definePort({
|
|
3
|
+
id: "apps.managed-auth",
|
|
4
|
+
test(runtime) {
|
|
5
|
+
if (!runtime || typeof runtime !== "object") {
|
|
6
|
+
throw new TypeError("apps.managed-auth must be an object.");
|
|
7
|
+
}
|
|
8
|
+
if (!runtime.runtimeAudience?.trim()) {
|
|
9
|
+
throw new TypeError("apps.managed-auth runtimeAudience must be a non-empty string.");
|
|
10
|
+
}
|
|
11
|
+
if (typeof runtime.sessionTokenSigningSecret !== "string" ||
|
|
12
|
+
runtime.sessionTokenSigningSecret.length < 32) {
|
|
13
|
+
throw new TypeError("apps.managed-auth sessionTokenSigningSecret must contain at least 32 characters.");
|
|
14
|
+
}
|
|
15
|
+
if (runtime.sessionTokenTtlSeconds !== undefined &&
|
|
16
|
+
(!Number.isInteger(runtime.sessionTokenTtlSeconds) ||
|
|
17
|
+
runtime.sessionTokenTtlSeconds <= 0 ||
|
|
18
|
+
runtime.sessionTokenTtlSeconds > 300)) {
|
|
19
|
+
throw new TypeError("apps.managed-auth sessionTokenTtlSeconds must be an integer from 1 through 300 when provided.");
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -15,6 +15,8 @@ export interface AppSessionTokenServiceOptions {
|
|
|
15
15
|
export interface IssueAppSessionTokenInput {
|
|
16
16
|
installationId: string;
|
|
17
17
|
viewerId: string;
|
|
18
|
+
/** Current host-authenticated viewer permissions. */
|
|
19
|
+
viewerScopes?: readonly string[];
|
|
18
20
|
entity?: AppSessionTokenEntity | null;
|
|
19
21
|
slot?: string | null;
|
|
20
22
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Both paths write `app_audit_events` so issuance and exchange are auditable.
|
|
14
14
|
*/
|
|
15
15
|
import { ApiHttpError } from "@voyant-travel/hono";
|
|
16
|
-
import { and, eq, isNull } from "drizzle-orm";
|
|
16
|
+
import { and, eq, gt, isNull } from "drizzle-orm";
|
|
17
17
|
import { appAuditEvents, appInstallations, appSessionTokens, } from "./schema.js";
|
|
18
18
|
import { signAppSessionToken, verifyAppSessionToken, } from "./session-token.js";
|
|
19
19
|
export function createAppSessionTokenService(options) {
|
|
@@ -25,6 +25,7 @@ export function createAppSessionTokenService(options) {
|
|
|
25
25
|
installationId: installation.id,
|
|
26
26
|
deploymentId: installation.deploymentId,
|
|
27
27
|
viewerId: input.viewerId,
|
|
28
|
+
viewerScopes: input.viewerScopes ?? [],
|
|
28
29
|
entity: input.entity ?? null,
|
|
29
30
|
slot: input.slot ?? null,
|
|
30
31
|
};
|
|
@@ -63,37 +64,53 @@ export function createAppSessionTokenService(options) {
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
const { claims } = verified;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
return db.transaction(async (tx) => {
|
|
68
|
+
const installation = await requireActiveInstallation(tx, claims.installationId);
|
|
69
|
+
if (installation.appId !== claims.aud || installation.deploymentId !== claims.deploymentId) {
|
|
70
|
+
throw new ApiHttpError("Session token installation context changed", {
|
|
71
|
+
status: 401,
|
|
72
|
+
code: "app_session_token_installation_mismatch",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Client authentication and token construction happen before consumption,
|
|
76
|
+
// but in the same transaction. If authentication/minting fails, or another
|
|
77
|
+
// exchange wins the JTI race, every credential side effect rolls back.
|
|
78
|
+
const tokens = await options.oauth.token(tx, {
|
|
79
|
+
grantType: "urn:voyant:params:oauth:grant-type:actor-token-exchange",
|
|
80
|
+
installationId: installation.id,
|
|
81
|
+
viewerId: claims.sub,
|
|
82
|
+
viewerScopes: intersectRequestedScopes(claims.viewerScopes, input.viewerScopes),
|
|
83
|
+
contextualScopes: input.contextualScopes,
|
|
84
|
+
contextConstraint: { entity: claims.entity, slot: claims.slot },
|
|
85
|
+
clientId: input.clientId,
|
|
86
|
+
clientSecret: input.clientSecret,
|
|
77
87
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const consumed = await tx
|
|
89
|
+
.update(appSessionTokens)
|
|
90
|
+
.set({ consumedAt: now(), consumedByActorId: claims.sub })
|
|
91
|
+
.where(and(eq(appSessionTokens.jti, claims.jti), eq(appSessionTokens.installationId, installation.id), eq(appSessionTokens.appId, claims.aud), eq(appSessionTokens.deploymentId, claims.deploymentId), eq(appSessionTokens.viewerId, claims.sub), isNull(appSessionTokens.consumedAt), gt(appSessionTokens.expiresAt, now())))
|
|
92
|
+
.returning({ id: appSessionTokens.id });
|
|
93
|
+
if (consumed.length === 0) {
|
|
94
|
+
throw new ApiHttpError("Session token has already been used", {
|
|
95
|
+
status: 401,
|
|
96
|
+
code: "app_session_token_replayed",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
await audit(tx, installation, claims.sub, "session-token.exchanged", {
|
|
100
|
+
jti: claims.jti,
|
|
101
|
+
slot: claims.slot,
|
|
102
|
+
entityType: claims.entity?.type ?? null,
|
|
103
|
+
entityId: claims.entity?.id ?? null,
|
|
104
|
+
});
|
|
105
|
+
return tokens;
|
|
92
106
|
});
|
|
93
|
-
return tokens;
|
|
94
107
|
}
|
|
95
108
|
return { issue, exchange };
|
|
96
109
|
}
|
|
110
|
+
function intersectRequestedScopes(trustedViewerScopes, requestedScopes) {
|
|
111
|
+
const trusted = new Set(trustedViewerScopes);
|
|
112
|
+
return Array.from(new Set(requestedScopes.filter((scope) => trusted.has(scope)))).sort();
|
|
113
|
+
}
|
|
97
114
|
async function requireActiveInstallation(db, installationId) {
|
|
98
115
|
const [installation] = await db
|
|
99
116
|
.select()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { ApiHttpError } from "@voyant-travel/hono";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { appOAuthTokenSchema } from "./contracts.js";
|
|
4
|
+
import { createAppSessionTokenService } from "./session-token-service.js";
|
|
5
|
+
function trackedDatabaseStub() {
|
|
6
|
+
const installation = {
|
|
7
|
+
id: "inst_1",
|
|
8
|
+
appId: "app_1",
|
|
9
|
+
releaseId: "rel_1",
|
|
10
|
+
deploymentId: "dep_1",
|
|
11
|
+
status: "active",
|
|
12
|
+
namespace: "app--one",
|
|
13
|
+
};
|
|
14
|
+
const consume = vi.fn().mockResolvedValue([{ id: "session_1" }]);
|
|
15
|
+
const db = {
|
|
16
|
+
select: () => ({
|
|
17
|
+
from: () => ({ where: () => ({ limit: async () => [installation] }) }),
|
|
18
|
+
}),
|
|
19
|
+
insert: () => ({ values: async () => undefined }),
|
|
20
|
+
update: () => ({
|
|
21
|
+
set: () => ({ where: () => ({ returning: consume }) }),
|
|
22
|
+
}),
|
|
23
|
+
transaction: async (callback) => callback(db),
|
|
24
|
+
};
|
|
25
|
+
return { db, consume };
|
|
26
|
+
}
|
|
27
|
+
function databaseStub() {
|
|
28
|
+
return trackedDatabaseStub().db;
|
|
29
|
+
}
|
|
30
|
+
describe("app session token service", () => {
|
|
31
|
+
it("does not expose an app-asserted viewer scope grant on the public OAuth schema", () => {
|
|
32
|
+
expect(appOAuthTokenSchema.safeParse({
|
|
33
|
+
grant_type: "urn:voyant:params:oauth:grant-type:actor-token-exchange",
|
|
34
|
+
installation_id: "inst_1",
|
|
35
|
+
viewer_id: "viewer_1",
|
|
36
|
+
viewer_scopes: ["finance-document-artifacts:write"],
|
|
37
|
+
client_id: "app_1",
|
|
38
|
+
}).success).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it("intersects app-requested scopes with host-signed viewer authority", async () => {
|
|
41
|
+
const token = vi.fn().mockResolvedValue({ accessToken: "online" });
|
|
42
|
+
const service = createAppSessionTokenService({
|
|
43
|
+
secret: "test-deployment-session-secret-value-000000000000",
|
|
44
|
+
deploymentId: "dep_1",
|
|
45
|
+
oauth: { token },
|
|
46
|
+
now: () => new Date("2026-07-18T09:00:00.000Z"),
|
|
47
|
+
});
|
|
48
|
+
const db = databaseStub();
|
|
49
|
+
const issued = await service.issue(db, {
|
|
50
|
+
installationId: "inst_1",
|
|
51
|
+
viewerId: "viewer_1",
|
|
52
|
+
viewerScopes: ["finance-documents:read", "finance-document-artifacts:write"],
|
|
53
|
+
entity: { type: "invoice", id: "invoice_1" },
|
|
54
|
+
slot: "invoice.details.after-summary",
|
|
55
|
+
});
|
|
56
|
+
await service.exchange(db, {
|
|
57
|
+
token: issued.token,
|
|
58
|
+
clientId: "app_1",
|
|
59
|
+
clientSecret: "secret",
|
|
60
|
+
viewerScopes: [
|
|
61
|
+
"finance-documents:read",
|
|
62
|
+
"finance-document-artifacts:write",
|
|
63
|
+
"finance-external-sync:write",
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
expect(token).toHaveBeenCalledWith(db, expect.objectContaining({
|
|
67
|
+
viewerId: "viewer_1",
|
|
68
|
+
viewerScopes: ["finance-document-artifacts:write", "finance-documents:read"],
|
|
69
|
+
contextConstraint: {
|
|
70
|
+
entity: { type: "invoice", id: "invoice_1" },
|
|
71
|
+
slot: "invoice.details.after-summary",
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
it("fails closed to no online scopes when issuance had no viewer authority", async () => {
|
|
76
|
+
const token = vi.fn().mockResolvedValue({ accessToken: "online" });
|
|
77
|
+
const service = createAppSessionTokenService({
|
|
78
|
+
secret: "test-deployment-session-secret-value-000000000000",
|
|
79
|
+
deploymentId: "dep_1",
|
|
80
|
+
oauth: { token },
|
|
81
|
+
now: () => new Date("2026-07-18T09:00:00.000Z"),
|
|
82
|
+
});
|
|
83
|
+
const db = databaseStub();
|
|
84
|
+
const issued = await service.issue(db, {
|
|
85
|
+
installationId: "inst_1",
|
|
86
|
+
viewerId: "viewer_1",
|
|
87
|
+
});
|
|
88
|
+
await service.exchange(db, {
|
|
89
|
+
token: issued.token,
|
|
90
|
+
clientId: "app_1",
|
|
91
|
+
viewerScopes: ["finance-document-artifacts:write"],
|
|
92
|
+
});
|
|
93
|
+
expect(token).toHaveBeenCalledWith(db, expect.objectContaining({ viewerScopes: [] }));
|
|
94
|
+
});
|
|
95
|
+
it("does not consume the JTI when app client authentication fails", async () => {
|
|
96
|
+
const token = vi.fn(async (_db, input) => {
|
|
97
|
+
if (input.clientSecret !== "right-secret") {
|
|
98
|
+
throw new ApiHttpError("Client authentication failed", {
|
|
99
|
+
status: 401,
|
|
100
|
+
code: "invalid_client",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { accessToken: "online" };
|
|
104
|
+
});
|
|
105
|
+
const service = createAppSessionTokenService({
|
|
106
|
+
secret: "test-deployment-session-secret-value-000000000000",
|
|
107
|
+
deploymentId: "dep_1",
|
|
108
|
+
oauth: { token },
|
|
109
|
+
now: () => new Date("2026-07-18T09:00:00.000Z"),
|
|
110
|
+
});
|
|
111
|
+
const { db, consume } = trackedDatabaseStub();
|
|
112
|
+
const issued = await service.issue(db, {
|
|
113
|
+
installationId: "inst_1",
|
|
114
|
+
viewerId: "viewer_1",
|
|
115
|
+
});
|
|
116
|
+
await expect(service.exchange(db, {
|
|
117
|
+
token: issued.token,
|
|
118
|
+
clientId: "app_1",
|
|
119
|
+
clientSecret: "wrong-secret",
|
|
120
|
+
viewerScopes: [],
|
|
121
|
+
})).rejects.toMatchObject({ code: "invalid_client" });
|
|
122
|
+
expect(consume).not.toHaveBeenCalled();
|
|
123
|
+
await expect(service.exchange(db, {
|
|
124
|
+
token: issued.token,
|
|
125
|
+
clientId: "app_1",
|
|
126
|
+
clientSecret: "right-secret",
|
|
127
|
+
viewerScopes: [],
|
|
128
|
+
})).resolves.toEqual({ accessToken: "online" });
|
|
129
|
+
expect(consume).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
it("leaves the JTI retryable when online credential minting fails", async () => {
|
|
132
|
+
const token = vi
|
|
133
|
+
.fn()
|
|
134
|
+
.mockRejectedValueOnce(new Error("credential insert failed"))
|
|
135
|
+
.mockResolvedValueOnce({ accessToken: "online" });
|
|
136
|
+
const service = createAppSessionTokenService({
|
|
137
|
+
secret: "test-deployment-session-secret-value-000000000000",
|
|
138
|
+
deploymentId: "dep_1",
|
|
139
|
+
oauth: { token },
|
|
140
|
+
now: () => new Date("2026-07-18T09:00:00.000Z"),
|
|
141
|
+
});
|
|
142
|
+
const { db, consume } = trackedDatabaseStub();
|
|
143
|
+
const issued = await service.issue(db, {
|
|
144
|
+
installationId: "inst_1",
|
|
145
|
+
viewerId: "viewer_1",
|
|
146
|
+
});
|
|
147
|
+
const input = {
|
|
148
|
+
token: issued.token,
|
|
149
|
+
clientId: "app_1",
|
|
150
|
+
viewerScopes: [],
|
|
151
|
+
};
|
|
152
|
+
await expect(service.exchange(db, input)).rejects.toThrow("credential insert failed");
|
|
153
|
+
expect(consume).not.toHaveBeenCalled();
|
|
154
|
+
await expect(service.exchange(db, input)).resolves.toEqual({ accessToken: "online" });
|
|
155
|
+
expect(consume).toHaveBeenCalledOnce();
|
|
156
|
+
});
|
|
157
|
+
});
|
package/dist/session-token.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare const APP_SESSION_TOKEN_PREFIX = "vsess_";
|
|
|
3
3
|
* HKDF context label under which admin session tokens are signed. Bumping the
|
|
4
4
|
* version suffix invalidates every outstanding token.
|
|
5
5
|
*/
|
|
6
|
-
export declare const APP_SESSION_TOKEN_KEY_CONTEXT = "voyant:app-session-token:
|
|
6
|
+
export declare const APP_SESSION_TOKEN_KEY_CONTEXT = "voyant:app-session-token:v2";
|
|
7
7
|
/** Default token lifetime — short enough to make replay low value. */
|
|
8
8
|
export declare const APP_SESSION_TOKEN_TTL_SECONDS = 120;
|
|
9
9
|
/** Stable issuer claim, distinguishing these from every other Voyant token. */
|
|
@@ -21,6 +21,8 @@ export interface AppSessionTokenContext {
|
|
|
21
21
|
deploymentId: string;
|
|
22
22
|
/** The admin viewer the token acts on behalf of. */
|
|
23
23
|
viewerId: string;
|
|
24
|
+
/** Host-authenticated viewer permissions captured at issuance. */
|
|
25
|
+
viewerScopes?: readonly string[];
|
|
24
26
|
entity?: AppSessionTokenEntity | null;
|
|
25
27
|
slot?: string | null;
|
|
26
28
|
}
|
|
@@ -35,6 +37,8 @@ export interface AppSessionTokenClaims {
|
|
|
35
37
|
deploymentId: string;
|
|
36
38
|
entity: AppSessionTokenEntity | null;
|
|
37
39
|
slot: string | null;
|
|
40
|
+
/** Host-authenticated viewer permissions; never supplied by the app backend. */
|
|
41
|
+
viewerScopes: string[];
|
|
38
42
|
iat: number;
|
|
39
43
|
exp: number;
|
|
40
44
|
/** Unique token id; the exchange store enforces single use by this value. */
|
package/dist/session-token.js
CHANGED
|
Binary file
|
|
@@ -6,6 +6,7 @@ const context = {
|
|
|
6
6
|
installationId: "apin_1",
|
|
7
7
|
deploymentId: "dep_1",
|
|
8
8
|
viewerId: "usr_1",
|
|
9
|
+
viewerScopes: ["finance-documents:read", "finance-document-artifacts:write"],
|
|
9
10
|
entity: { type: "booking", id: "book_1" },
|
|
10
11
|
slot: "booking.details.header",
|
|
11
12
|
};
|
|
@@ -27,6 +28,10 @@ describe("app session token", () => {
|
|
|
27
28
|
expect(result.claims.installationId).toBe("apin_1");
|
|
28
29
|
expect(result.claims.entity).toEqual({ type: "booking", id: "book_1" });
|
|
29
30
|
expect(result.claims.slot).toBe("booking.details.header");
|
|
31
|
+
expect(result.claims.viewerScopes).toEqual([
|
|
32
|
+
"finance-document-artifacts:write",
|
|
33
|
+
"finance-documents:read",
|
|
34
|
+
]);
|
|
30
35
|
expect(result.claims.jti).toMatch(/^st_/);
|
|
31
36
|
});
|
|
32
37
|
it("mints a distinct jti per issuance", () => {
|
package/dist/voyant.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { defineModule, requirePort } from "@voyant-travel/core/project";
|
|
1
|
+
import { defineModule, providePort, requirePort } from "@voyant-travel/core/project";
|
|
2
2
|
import { customFieldValueLifecycleRuntimePort, customFieldValueOperationsRuntimePort, } from "@voyant-travel/core/runtime-port";
|
|
3
|
+
import { financeAppApiRuntimePort } from "@voyant-travel/finance-contracts/runtime-port";
|
|
4
|
+
import { appsManagedAuthRuntimePort } from "./runtime-port.js";
|
|
3
5
|
const appsAdminRuntime = {
|
|
4
6
|
entry: "@voyant-travel/apps-react/admin",
|
|
5
7
|
export: "createSelectedAppsAdminExtension",
|
|
@@ -27,7 +29,12 @@ export const appsVoyantModule = defineModule({
|
|
|
27
29
|
optional: true,
|
|
28
30
|
cardinality: "many",
|
|
29
31
|
}),
|
|
32
|
+
requirePort(financeAppApiRuntimePort, { optional: true }),
|
|
33
|
+
requirePort(appsManagedAuthRuntimePort, { optional: true }),
|
|
30
34
|
],
|
|
35
|
+
provides: {
|
|
36
|
+
ports: [providePort(appsManagedAuthRuntimePort)],
|
|
37
|
+
},
|
|
31
38
|
api: [
|
|
32
39
|
{
|
|
33
40
|
id: "@voyant-travel/apps#api.admin",
|
|
@@ -54,6 +61,27 @@ export const appsVoyantModule = defineModule({
|
|
|
54
61
|
source: "./migrations",
|
|
55
62
|
},
|
|
56
63
|
],
|
|
64
|
+
config: [
|
|
65
|
+
{
|
|
66
|
+
id: "@voyant-travel/apps#config.runtime-audience",
|
|
67
|
+
key: "VOYANT_APP_RUNTIME_AUDIENCE",
|
|
68
|
+
required: false,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "@voyant-travel/apps#config.session-token-ttl-seconds",
|
|
72
|
+
key: "VOYANT_APP_SESSION_TOKEN_TTL_SECONDS",
|
|
73
|
+
required: false,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
secrets: [
|
|
77
|
+
{
|
|
78
|
+
id: "@voyant-travel/apps#secret.session-token-signing-secret",
|
|
79
|
+
key: "VOYANT_APP_SESSION_TOKEN_SIGNING_SECRET",
|
|
80
|
+
required: false,
|
|
81
|
+
description: "Signing material for short-lived remote extension session tokens.",
|
|
82
|
+
rotation: "replace-only",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
57
85
|
events: [
|
|
58
86
|
{
|
|
59
87
|
id: "@voyant-travel/apps#event.installation-active",
|
|
@@ -259,6 +287,113 @@ export const appsVoyantModule = defineModule({
|
|
|
259
287
|
],
|
|
260
288
|
wildcard: "explicit-resource",
|
|
261
289
|
},
|
|
290
|
+
{
|
|
291
|
+
id: "@voyant-travel/apps#access.finance-external-references",
|
|
292
|
+
resource: "finance-external-references",
|
|
293
|
+
label: "Finance external references",
|
|
294
|
+
description: "Read and record provider-owned finance document references.",
|
|
295
|
+
remoteSafe: true,
|
|
296
|
+
actions: [
|
|
297
|
+
{
|
|
298
|
+
action: "read",
|
|
299
|
+
label: "Read external references",
|
|
300
|
+
description: "Read the app provider's reference for a finance document.",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
action: "write",
|
|
304
|
+
label: "Write external references",
|
|
305
|
+
description: "Idempotently record the app provider's finance document reference.",
|
|
306
|
+
sensitive: true,
|
|
307
|
+
wildcard: "explicit",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
wildcard: "explicit-resource",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: "@voyant-travel/apps#access.finance-external-allocation",
|
|
314
|
+
resource: "finance-external-allocation",
|
|
315
|
+
label: "External finance number allocation",
|
|
316
|
+
description: "Write a provider-owned number to a pending finance document.",
|
|
317
|
+
remoteSafe: true,
|
|
318
|
+
actions: [
|
|
319
|
+
{
|
|
320
|
+
action: "write",
|
|
321
|
+
label: "Allocate external finance number",
|
|
322
|
+
description: "Atomically finalize a pending provider-owned document number.",
|
|
323
|
+
sensitive: true,
|
|
324
|
+
wildcard: "explicit",
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
wildcard: "explicit-resource",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "@voyant-travel/apps#access.finance-document-artifacts",
|
|
331
|
+
resource: "finance-document-artifacts",
|
|
332
|
+
label: "Finance document artifacts",
|
|
333
|
+
description: "Attach private provider-generated renditions to finance documents.",
|
|
334
|
+
remoteSafe: true,
|
|
335
|
+
actions: [
|
|
336
|
+
{
|
|
337
|
+
action: "write",
|
|
338
|
+
label: "Attach finance document artifacts",
|
|
339
|
+
description: "Upload and bind a bounded provider-generated finance document PDF.",
|
|
340
|
+
sensitive: true,
|
|
341
|
+
wildcard: "explicit",
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
wildcard: "explicit-resource",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "@voyant-travel/apps#access.finance-external-sync",
|
|
348
|
+
resource: "finance-external-sync",
|
|
349
|
+
label: "Finance external synchronization",
|
|
350
|
+
description: "Report provider-neutral synchronization outcomes for finance documents.",
|
|
351
|
+
remoteSafe: true,
|
|
352
|
+
actions: [
|
|
353
|
+
{
|
|
354
|
+
action: "write",
|
|
355
|
+
label: "Report finance synchronization",
|
|
356
|
+
description: "Record ordered success, retryable-failure, or terminal-failure state.",
|
|
357
|
+
sensitive: true,
|
|
358
|
+
wildcard: "explicit",
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
wildcard: "explicit-resource",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "@voyant-travel/apps#access.finance-external-lifecycle",
|
|
365
|
+
resource: "finance-external-lifecycle",
|
|
366
|
+
label: "Finance external lifecycle",
|
|
367
|
+
description: "Report provider-neutral lifecycle facts for finance documents.",
|
|
368
|
+
remoteSafe: true,
|
|
369
|
+
actions: [
|
|
370
|
+
{
|
|
371
|
+
action: "write",
|
|
372
|
+
label: "Report finance lifecycle",
|
|
373
|
+
description: "Record an ordered conversion or void observation.",
|
|
374
|
+
sensitive: true,
|
|
375
|
+
wildcard: "explicit",
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
wildcard: "explicit-resource",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "@voyant-travel/apps#access.finance-settlement-observations",
|
|
382
|
+
resource: "finance-settlement-observations",
|
|
383
|
+
label: "Finance settlement observations",
|
|
384
|
+
description: "Report provider-neutral settlement evidence without creating payments.",
|
|
385
|
+
remoteSafe: true,
|
|
386
|
+
actions: [
|
|
387
|
+
{
|
|
388
|
+
action: "write",
|
|
389
|
+
label: "Report settlement observations",
|
|
390
|
+
description: "Record ordered partial or paid observations and payment identifiers.",
|
|
391
|
+
sensitive: true,
|
|
392
|
+
wildcard: "explicit",
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
wildcard: "explicit-resource",
|
|
396
|
+
},
|
|
262
397
|
{
|
|
263
398
|
id: "@voyant-travel/apps#access.apps",
|
|
264
399
|
resource: "apps",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -89,6 +89,11 @@
|
|
|
89
89
|
"import": "./dist/runtime-contributor.js",
|
|
90
90
|
"default": "./dist/runtime-contributor.js"
|
|
91
91
|
},
|
|
92
|
+
"./runtime-port": {
|
|
93
|
+
"types": "./dist/runtime-port.d.ts",
|
|
94
|
+
"import": "./dist/runtime-port.js",
|
|
95
|
+
"default": "./dist/runtime-port.js"
|
|
96
|
+
},
|
|
92
97
|
"./schema": {
|
|
93
98
|
"types": "./dist/schema.d.ts",
|
|
94
99
|
"import": "./dist/schema.js",
|
|
@@ -124,9 +129,10 @@
|
|
|
124
129
|
"@voyant-travel/admin": "^0.127.0",
|
|
125
130
|
"@voyant-travel/admin-extension-sdk": "^0.2.0",
|
|
126
131
|
"@voyant-travel/core": "^0.127.0",
|
|
127
|
-
"@voyant-travel/custom-fields": "^0.2.
|
|
132
|
+
"@voyant-travel/custom-fields": "^0.2.4",
|
|
133
|
+
"@voyant-travel/finance-contracts": "^0.107.0",
|
|
128
134
|
"@voyant-travel/db": "^0.114.13",
|
|
129
|
-
"@voyant-travel/hono": "^0.
|
|
135
|
+
"@voyant-travel/hono": "^0.129.0",
|
|
130
136
|
"@voyant-travel/types": "^0.109.4",
|
|
131
137
|
"@voyant-travel/webhook-delivery": "^0.4.2"
|
|
132
138
|
},
|