@voyant-travel/apps 0.7.0 → 0.9.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/access-boundary.d.ts +2 -0
- package/dist/api-runtime.d.ts +23 -11
- package/dist/api-runtime.js +40 -1
- package/dist/api-runtime.test.d.ts +1 -0
- package/dist/api-runtime.test.js +58 -0
- package/dist/app-api-contracts.d.ts +58 -8
- package/dist/app-api-contracts.js +102 -9
- package/dist/app-api-finance-routes.test.js +482 -2
- package/dist/app-api-routes.d.ts +6 -5
- package/dist/app-api-routes.js +131 -22
- package/dist/app-api-service.d.ts +33 -1
- package/dist/app-api-service.js +162 -0
- package/dist/compiler.test.js +7 -0
- package/dist/consent.d.ts +1 -0
- package/dist/consent.js +2 -2
- package/dist/contracts.d.ts +2 -26
- package/dist/contracts.js +4 -11
- package/dist/installation-service.d.ts +27 -3
- package/dist/installation-service.js +72 -3
- package/dist/oauth-installation.d.ts +101 -0
- package/dist/oauth-installation.js +143 -0
- package/dist/oauth-service.d.ts +9 -1
- package/dist/oauth-service.js +111 -97
- package/dist/oauth-service.test.js +121 -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 +8 -5
- package/dist/routes.js +43 -24
- package/dist/runtime-contributor.d.ts +13 -1
- package/dist/runtime-contributor.js +9 -1
- package/dist/runtime-contributor.test.d.ts +1 -0
- package/dist/runtime-contributor.test.js +55 -0
- package/dist/runtime-port.d.ts +29 -0
- package/dist/runtime-port.js +28 -0
- package/dist/schema.d.ts +170 -0
- package/dist/schema.js +18 -0
- package/dist/session-token-service.d.ts +4 -0
- package/dist/session-token-service.js +90 -28
- package/dist/session-token-service.test.d.ts +1 -0
- package/dist/session-token-service.test.js +187 -0
- package/dist/session-token.d.ts +13 -2
- package/dist/session-token.js +0 -0
- package/dist/session-token.test.js +51 -0
- package/dist/voyant.js +95 -1
- package/migrations/20260718205748_managed_installation_binding.sql +16 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +10 -5
package/dist/routes.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import { openApiValidationHook, parseJsonBody, parseQuery, RequestValidationError, requireUserId, } from "@voyant-travel/hono";
|
|
3
|
+
import { hasApiKeyPermission, permissionStringsToPermissions, } from "@voyant-travel/types/api-keys";
|
|
3
4
|
import { z } from "zod";
|
|
5
|
+
import { grantableRemoteAppScopes } from "./consent.js";
|
|
4
6
|
import { activateInstallationBodySchema, appCredentialRevocationSchema, appInstallationAuditQuerySchema, appInstallationListQuerySchema, appListQuerySchema, appOAuthAuthorizeQuerySchema, appOAuthTokenSchema, appSessionTokenExchangeSchema, appSessionTokenIssueSchema, appWebhookReplaySchema, createCustomAppRegistrationSchema, installAppSchema, lifecycleActionBodySchema, releaseManifestFetchSchema, releaseManifestUploadSchema, } from "./contracts.js";
|
|
5
7
|
import { listAppReleases, listInstallationAudit, listInstallationSummaries, loadInstallationDetail, } from "./installation-read-model.js";
|
|
6
8
|
import { createAppInstallationService } from "./installation-service.js";
|
|
@@ -16,16 +18,19 @@ export function createAppsAdminRoutes(options = {}) {
|
|
|
16
18
|
const service = createAppsService(options);
|
|
17
19
|
const installations = createAppInstallationService({
|
|
18
20
|
deploymentId: options.oauth?.deploymentId ?? options.deploymentId,
|
|
21
|
+
managedInstallation: options.oauth?.managedInstallation,
|
|
19
22
|
platformApiVersion: options.platformApiVersion,
|
|
20
23
|
eventBus: options.eventBus,
|
|
21
24
|
customFields: options.customFields,
|
|
22
25
|
});
|
|
23
26
|
const oauth = options.oauth ? createAppOAuthService(options.oauth) : null;
|
|
27
|
+
const oauthAccessCatalog = options.oauth?.accessCatalog;
|
|
24
28
|
const sessionTokens = oauth && options.oauth && options.sessionToken
|
|
25
29
|
? createAppSessionTokenService({
|
|
26
30
|
secret: options.sessionToken.secret,
|
|
27
31
|
ttlSeconds: options.sessionToken.ttlSeconds,
|
|
28
32
|
deploymentId: options.oauth.deploymentId,
|
|
33
|
+
managedInstallation: options.sessionToken.managedInstallation ?? options.oauth.managedInstallation,
|
|
29
34
|
oauth,
|
|
30
35
|
})
|
|
31
36
|
: null;
|
|
@@ -42,8 +47,9 @@ export function createAppsAdminRoutes(options = {}) {
|
|
|
42
47
|
// so it must never be reachable through read-scoped GET requests. The admin
|
|
43
48
|
// consent UI submits the approval and performs the redirect itself.
|
|
44
49
|
routes.openapi(authorizeAppOAuthRoute, async (c) => {
|
|
45
|
-
if (!oauth)
|
|
50
|
+
if (!oauth || !oauthAccessCatalog) {
|
|
46
51
|
return c.json({ error: "App OAuth is not configured" }, 501);
|
|
52
|
+
}
|
|
47
53
|
const body = await parseJsonBody(c, appOAuthAuthorizeQuerySchema);
|
|
48
54
|
const result = await oauth.authorize(c.get("db"), {
|
|
49
55
|
appId: body.client_id,
|
|
@@ -52,8 +58,8 @@ export function createAppsAdminRoutes(options = {}) {
|
|
|
52
58
|
state: body.state,
|
|
53
59
|
codeChallenge: body.code_challenge,
|
|
54
60
|
codeChallengeMethod: body.code_challenge_method,
|
|
55
|
-
actorId:
|
|
56
|
-
operatorGrantedScopes:
|
|
61
|
+
actorId: requireUserId(c),
|
|
62
|
+
operatorGrantedScopes: resolveOperatorGrantableRemoteAppScopes(c.get("scopes") ?? [], oauthAccessCatalog),
|
|
57
63
|
grantedOptionalScopes: splitScopes(body.optional_scopes),
|
|
58
64
|
});
|
|
59
65
|
const redirectUrl = new URL(result.redirectUri);
|
|
@@ -74,22 +80,12 @@ export function createAppsAdminRoutes(options = {}) {
|
|
|
74
80
|
clientId: body.client_id,
|
|
75
81
|
clientSecret: body.client_secret,
|
|
76
82
|
})
|
|
77
|
-
:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
})
|
|
84
|
-
: await oauth.token(c.get("db"), {
|
|
85
|
-
grantType: "urn:voyant:params:oauth:grant-type:actor-token-exchange",
|
|
86
|
-
installationId: body.installation_id,
|
|
87
|
-
viewerId: body.viewer_id,
|
|
88
|
-
viewerScopes: body.viewer_scopes,
|
|
89
|
-
contextualScopes: body.contextual_scopes,
|
|
90
|
-
clientId: body.client_id,
|
|
91
|
-
clientSecret: body.client_secret,
|
|
92
|
-
});
|
|
83
|
+
: await oauth.token(c.get("db"), {
|
|
84
|
+
grantType: "refresh_token",
|
|
85
|
+
refreshToken: body.refresh_token,
|
|
86
|
+
clientId: body.client_id,
|
|
87
|
+
clientSecret: body.client_secret,
|
|
88
|
+
});
|
|
93
89
|
return c.json(token, 200);
|
|
94
90
|
});
|
|
95
91
|
routes.openapi(revokeAppInstallationCredentialsRoute, async (c) => {
|
|
@@ -111,6 +107,7 @@ export function createAppsAdminRoutes(options = {}) {
|
|
|
111
107
|
const issued = await sessionTokens.issue(c.get("db"), {
|
|
112
108
|
installationId,
|
|
113
109
|
viewerId,
|
|
110
|
+
viewerScopes: resolveViewerRemoteAppScopes(c.get("scopes") ?? [], options.oauth?.accessCatalog),
|
|
114
111
|
entity: body.entity ?? null,
|
|
115
112
|
slot: body.slot ?? null,
|
|
116
113
|
});
|
|
@@ -133,11 +130,10 @@ export function createAppsAdminRoutes(options = {}) {
|
|
|
133
130
|
});
|
|
134
131
|
routes.openapi(installAppRoute, async (c) => {
|
|
135
132
|
const body = await parseJsonBody(c, installAppSchema);
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
const deploymentId = body.deploymentId ?? c.env?.VOYANT_CLOUD_DEPLOYMENT_ID?.trim() ?? options.deploymentId;
|
|
133
|
+
// A configured managed runtime audience is authoritative so installation
|
|
134
|
+
// identity cannot diverge from later OAuth token audiences. Direct/custom
|
|
135
|
+
// hosts without managed auth may still supply an explicit deployment ID.
|
|
136
|
+
const deploymentId = options.oauth?.deploymentId ?? body.deploymentId ?? options.deploymentId;
|
|
141
137
|
const result = await installations.install(c.get("db"), {
|
|
142
138
|
appId: body.appId,
|
|
143
139
|
releaseId: body.releaseId,
|
|
@@ -248,6 +244,29 @@ export function createAppsAdminRoutes(options = {}) {
|
|
|
248
244
|
});
|
|
249
245
|
return routes;
|
|
250
246
|
}
|
|
247
|
+
export function resolveViewerRemoteAppScopes(scopes, catalog) {
|
|
248
|
+
if (!catalog)
|
|
249
|
+
return [];
|
|
250
|
+
const permissions = permissionStringsToPermissions(scopes);
|
|
251
|
+
return catalog.resources
|
|
252
|
+
.flatMap((resource) => resource.actions
|
|
253
|
+
.filter((action) => (resource.remoteSafe || action.remoteSafe) &&
|
|
254
|
+
hasApiKeyPermission(permissions, resource.resource, action.action, catalog))
|
|
255
|
+
.map((action) => `${resource.resource}:${action.action}`))
|
|
256
|
+
.sort();
|
|
257
|
+
}
|
|
258
|
+
export function resolveOperatorGrantableRemoteAppScopes(scopes, catalog) {
|
|
259
|
+
const grantable = grantableRemoteAppScopes(catalog);
|
|
260
|
+
const permissions = permissionStringsToPermissions(scopes);
|
|
261
|
+
return [...grantable]
|
|
262
|
+
.filter((scope) => {
|
|
263
|
+
const separator = scope.lastIndexOf(":");
|
|
264
|
+
if (separator <= 0)
|
|
265
|
+
return false;
|
|
266
|
+
return hasApiKeyPermission(permissions, scope.slice(0, separator), scope.slice(separator + 1), catalog);
|
|
267
|
+
})
|
|
268
|
+
.sort();
|
|
269
|
+
}
|
|
251
270
|
function splitScopes(value) {
|
|
252
271
|
return value
|
|
253
272
|
.split(/[,\s]+/)
|
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import type { VoyantRuntimeHostPrimitives } from "@voyant-travel/core";
|
|
2
|
+
export interface AppsRuntimeContributorHost {
|
|
3
|
+
primitives: Pick<VoyantRuntimeHostPrimitives, "config">;
|
|
4
|
+
hasRuntimePort?(port: {
|
|
5
|
+
id: string;
|
|
6
|
+
}): boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Managed installation contracts are host authority and cannot be reconstructed
|
|
10
|
+
* from scalar environment values. Hosts contribute `apps.managed-auth`
|
|
11
|
+
* explicitly; self-hosted runtimes keep the port absent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createAppsRuntimePortContribution(host: AppsRuntimeContributorHost): Readonly<Record<string, unknown>>;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { appsManagedAuthRuntimePort } from "./runtime-port.js";
|
|
2
|
+
/**
|
|
3
|
+
* Managed installation contracts are host authority and cannot be reconstructed
|
|
4
|
+
* from scalar environment values. Hosts contribute `apps.managed-auth`
|
|
5
|
+
* explicitly; self-hosted runtimes keep the port absent.
|
|
6
|
+
*/
|
|
7
|
+
export function createAppsRuntimePortContribution(host) {
|
|
8
|
+
if (host.hasRuntimePort?.(appsManagedAuthRuntimePort))
|
|
9
|
+
return {};
|
|
2
10
|
return {};
|
|
3
11
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createAppsRuntimePortContribution } from "./runtime-contributor.js";
|
|
3
|
+
import { appsManagedAuthRuntimePort } from "./runtime-port.js";
|
|
4
|
+
function host(values) {
|
|
5
|
+
return {
|
|
6
|
+
hasRuntimePort: () => false,
|
|
7
|
+
primitives: {
|
|
8
|
+
config: { read: (_bindings, key) => values[key] },
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe("createAppsRuntimePortContribution", () => {
|
|
13
|
+
it("stays off because scalar environment values cannot authorize managed installations", () => {
|
|
14
|
+
expect(createAppsRuntimePortContribution(host({}))).toEqual({});
|
|
15
|
+
expect(createAppsRuntimePortContribution(host({
|
|
16
|
+
VOYANT_APP_RUNTIME_AUDIENCE: "deployment-1",
|
|
17
|
+
VOYANT_APP_SESSION_TOKEN_SIGNING_SECRET: "s".repeat(32),
|
|
18
|
+
}))).toEqual({});
|
|
19
|
+
});
|
|
20
|
+
it("does not replace an explicitly host-provided managed-auth port", () => {
|
|
21
|
+
const contribution = createAppsRuntimePortContribution({
|
|
22
|
+
...host({
|
|
23
|
+
VOYANT_APP_RUNTIME_AUDIENCE: "deployment-from-env",
|
|
24
|
+
VOYANT_APP_SESSION_TOKEN_SIGNING_SECRET: "s".repeat(32),
|
|
25
|
+
}),
|
|
26
|
+
hasRuntimePort: (port) => port.id === appsManagedAuthRuntimePort.id,
|
|
27
|
+
});
|
|
28
|
+
expect(contribution).toEqual({});
|
|
29
|
+
});
|
|
30
|
+
it("resolves independent per-app generations within one workload environment", async () => {
|
|
31
|
+
const generations = new Map([
|
|
32
|
+
["app_1", 2],
|
|
33
|
+
["app_2", 9],
|
|
34
|
+
]);
|
|
35
|
+
const runtime = {
|
|
36
|
+
runtimeAudience: "runtime-audience",
|
|
37
|
+
installationAuthority: {
|
|
38
|
+
workloadEnvironmentId: "workload_environment_1",
|
|
39
|
+
resolveInstallationContract: async ({ appId }) => ({
|
|
40
|
+
contractGeneration: generations.get(appId) ?? 0,
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
sessionTokenSigningSecret: "s".repeat(32),
|
|
44
|
+
};
|
|
45
|
+
expect(() => appsManagedAuthRuntimePort.test(runtime)).not.toThrow();
|
|
46
|
+
await expect(runtime.installationAuthority.resolveInstallationContract({
|
|
47
|
+
appId: "app_1",
|
|
48
|
+
releaseId: "release_1",
|
|
49
|
+
})).resolves.toEqual({ contractGeneration: 2 });
|
|
50
|
+
await expect(runtime.installationAuthority.resolveInstallationContract({
|
|
51
|
+
appId: "app_2",
|
|
52
|
+
releaseId: "release_2",
|
|
53
|
+
})).resolves.toEqual({ contractGeneration: 9 });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Persisted host contract for one managed app installation. */
|
|
2
|
+
export interface ManagedAppInstallationBinding {
|
|
3
|
+
workloadEnvironmentId: string;
|
|
4
|
+
contractGeneration: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ManagedAppInstallationContractInput {
|
|
7
|
+
appId: string;
|
|
8
|
+
releaseId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ManagedAppInstallationContract {
|
|
11
|
+
/** Per-app monotonic generation. Advancing it invalidates prior app credentials. */
|
|
12
|
+
contractGeneration: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ManagedAppInstallationAuthority {
|
|
15
|
+
/** Stable opaque workload-environment identity across runtime rollouts. */
|
|
16
|
+
workloadEnvironmentId: string;
|
|
17
|
+
/** Resolve the current admitted contract for this specific app release. */
|
|
18
|
+
resolveInstallationContract(input: ManagedAppInstallationContractInput): Promise<ManagedAppInstallationContract | null>;
|
|
19
|
+
}
|
|
20
|
+
export interface AppsManagedAuthRuntime {
|
|
21
|
+
/** Stable audience shared by authorization, installations, and session tokens. */
|
|
22
|
+
runtimeAudience: string;
|
|
23
|
+
/** Provider-neutral managed installation authority supplied by the host. */
|
|
24
|
+
installationAuthority: ManagedAppInstallationAuthority;
|
|
25
|
+
/** Host-owned HMAC material for short-lived extension session tokens. */
|
|
26
|
+
sessionTokenSigningSecret: string;
|
|
27
|
+
sessionTokenTtlSeconds?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const appsManagedAuthRuntimePort: import("@voyant-travel/core/project").VoyantPort<AppsManagedAuthRuntime>;
|
|
@@ -0,0 +1,28 @@
|
|
|
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 (!runtime.installationAuthority?.workloadEnvironmentId?.trim()) {
|
|
12
|
+
throw new TypeError("apps.managed-auth installationAuthority.workloadEnvironmentId must be a non-empty string.");
|
|
13
|
+
}
|
|
14
|
+
if (typeof runtime.installationAuthority.resolveInstallationContract !== "function") {
|
|
15
|
+
throw new TypeError("apps.managed-auth installationAuthority.resolveInstallationContract must be a function.");
|
|
16
|
+
}
|
|
17
|
+
if (typeof runtime.sessionTokenSigningSecret !== "string" ||
|
|
18
|
+
runtime.sessionTokenSigningSecret.length < 32) {
|
|
19
|
+
throw new TypeError("apps.managed-auth sessionTokenSigningSecret must contain at least 32 characters.");
|
|
20
|
+
}
|
|
21
|
+
if (runtime.sessionTokenTtlSeconds !== undefined &&
|
|
22
|
+
(!Number.isInteger(runtime.sessionTokenTtlSeconds) ||
|
|
23
|
+
runtime.sessionTokenTtlSeconds <= 0 ||
|
|
24
|
+
runtime.sessionTokenTtlSeconds > 300)) {
|
|
25
|
+
throw new TypeError("apps.managed-auth sessionTokenTtlSeconds must be an integer from 1 through 300 when provided.");
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
package/dist/schema.d.ts
CHANGED
|
@@ -995,6 +995,40 @@ export declare const appInstallations: import("drizzle-orm/pg-core").PgTableWith
|
|
|
995
995
|
identity: undefined;
|
|
996
996
|
generated: undefined;
|
|
997
997
|
}, {}, {}>;
|
|
998
|
+
workloadEnvironmentId: import("drizzle-orm/pg-core").PgColumn<{
|
|
999
|
+
name: "workload_environment_id";
|
|
1000
|
+
tableName: "app_installations";
|
|
1001
|
+
dataType: "string";
|
|
1002
|
+
columnType: "PgText";
|
|
1003
|
+
data: string;
|
|
1004
|
+
driverParam: string;
|
|
1005
|
+
notNull: false;
|
|
1006
|
+
hasDefault: false;
|
|
1007
|
+
isPrimaryKey: false;
|
|
1008
|
+
isAutoincrement: false;
|
|
1009
|
+
hasRuntimeDefault: false;
|
|
1010
|
+
enumValues: [string, ...string[]];
|
|
1011
|
+
baseColumn: never;
|
|
1012
|
+
identity: undefined;
|
|
1013
|
+
generated: undefined;
|
|
1014
|
+
}, {}, {}>;
|
|
1015
|
+
contractGeneration: import("drizzle-orm/pg-core").PgColumn<{
|
|
1016
|
+
name: "contract_generation";
|
|
1017
|
+
tableName: "app_installations";
|
|
1018
|
+
dataType: "number";
|
|
1019
|
+
columnType: "PgInteger";
|
|
1020
|
+
data: number;
|
|
1021
|
+
driverParam: string | number;
|
|
1022
|
+
notNull: false;
|
|
1023
|
+
hasDefault: false;
|
|
1024
|
+
isPrimaryKey: false;
|
|
1025
|
+
isAutoincrement: false;
|
|
1026
|
+
hasRuntimeDefault: false;
|
|
1027
|
+
enumValues: undefined;
|
|
1028
|
+
baseColumn: never;
|
|
1029
|
+
identity: undefined;
|
|
1030
|
+
generated: undefined;
|
|
1031
|
+
}, {}, {}>;
|
|
998
1032
|
releaseId: import("drizzle-orm/pg-core").PgColumn<{
|
|
999
1033
|
name: "release_id";
|
|
1000
1034
|
tableName: "app_installations";
|
|
@@ -1519,6 +1553,40 @@ export declare const appAccessCredentials: import("drizzle-orm/pg-core").PgTable
|
|
|
1519
1553
|
identity: undefined;
|
|
1520
1554
|
generated: undefined;
|
|
1521
1555
|
}, {}, {}>;
|
|
1556
|
+
workloadEnvironmentId: import("drizzle-orm/pg-core").PgColumn<{
|
|
1557
|
+
name: "workload_environment_id";
|
|
1558
|
+
tableName: "app_access_credentials";
|
|
1559
|
+
dataType: "string";
|
|
1560
|
+
columnType: "PgText";
|
|
1561
|
+
data: string;
|
|
1562
|
+
driverParam: string;
|
|
1563
|
+
notNull: false;
|
|
1564
|
+
hasDefault: false;
|
|
1565
|
+
isPrimaryKey: false;
|
|
1566
|
+
isAutoincrement: false;
|
|
1567
|
+
hasRuntimeDefault: false;
|
|
1568
|
+
enumValues: [string, ...string[]];
|
|
1569
|
+
baseColumn: never;
|
|
1570
|
+
identity: undefined;
|
|
1571
|
+
generated: undefined;
|
|
1572
|
+
}, {}, {}>;
|
|
1573
|
+
contractGeneration: import("drizzle-orm/pg-core").PgColumn<{
|
|
1574
|
+
name: "contract_generation";
|
|
1575
|
+
tableName: "app_access_credentials";
|
|
1576
|
+
dataType: "number";
|
|
1577
|
+
columnType: "PgInteger";
|
|
1578
|
+
data: number;
|
|
1579
|
+
driverParam: string | number;
|
|
1580
|
+
notNull: false;
|
|
1581
|
+
hasDefault: false;
|
|
1582
|
+
isPrimaryKey: false;
|
|
1583
|
+
isAutoincrement: false;
|
|
1584
|
+
hasRuntimeDefault: false;
|
|
1585
|
+
enumValues: undefined;
|
|
1586
|
+
baseColumn: never;
|
|
1587
|
+
identity: undefined;
|
|
1588
|
+
generated: undefined;
|
|
1589
|
+
}, {}, {}>;
|
|
1522
1590
|
tokenMode: import("drizzle-orm/pg-core").PgColumn<{
|
|
1523
1591
|
name: "token_mode";
|
|
1524
1592
|
tableName: "app_access_credentials";
|
|
@@ -1766,6 +1834,40 @@ export declare const appOAuthAuthorizationCodes: import("drizzle-orm/pg-core").P
|
|
|
1766
1834
|
identity: undefined;
|
|
1767
1835
|
generated: undefined;
|
|
1768
1836
|
}, {}, {}>;
|
|
1837
|
+
workloadEnvironmentId: import("drizzle-orm/pg-core").PgColumn<{
|
|
1838
|
+
name: "workload_environment_id";
|
|
1839
|
+
tableName: "app_oauth_authorization_codes";
|
|
1840
|
+
dataType: "string";
|
|
1841
|
+
columnType: "PgText";
|
|
1842
|
+
data: string;
|
|
1843
|
+
driverParam: string;
|
|
1844
|
+
notNull: false;
|
|
1845
|
+
hasDefault: false;
|
|
1846
|
+
isPrimaryKey: false;
|
|
1847
|
+
isAutoincrement: false;
|
|
1848
|
+
hasRuntimeDefault: false;
|
|
1849
|
+
enumValues: [string, ...string[]];
|
|
1850
|
+
baseColumn: never;
|
|
1851
|
+
identity: undefined;
|
|
1852
|
+
generated: undefined;
|
|
1853
|
+
}, {}, {}>;
|
|
1854
|
+
contractGeneration: import("drizzle-orm/pg-core").PgColumn<{
|
|
1855
|
+
name: "contract_generation";
|
|
1856
|
+
tableName: "app_oauth_authorization_codes";
|
|
1857
|
+
dataType: "number";
|
|
1858
|
+
columnType: "PgInteger";
|
|
1859
|
+
data: number;
|
|
1860
|
+
driverParam: string | number;
|
|
1861
|
+
notNull: false;
|
|
1862
|
+
hasDefault: false;
|
|
1863
|
+
isPrimaryKey: false;
|
|
1864
|
+
isAutoincrement: false;
|
|
1865
|
+
hasRuntimeDefault: false;
|
|
1866
|
+
enumValues: undefined;
|
|
1867
|
+
baseColumn: never;
|
|
1868
|
+
identity: undefined;
|
|
1869
|
+
generated: undefined;
|
|
1870
|
+
}, {}, {}>;
|
|
1769
1871
|
codeHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
1770
1872
|
name: "code_hash";
|
|
1771
1873
|
tableName: "app_oauth_authorization_codes";
|
|
@@ -2051,6 +2153,40 @@ export declare const appOAuthRefreshTokens: import("drizzle-orm/pg-core").PgTabl
|
|
|
2051
2153
|
identity: undefined;
|
|
2052
2154
|
generated: undefined;
|
|
2053
2155
|
}, {}, {}>;
|
|
2156
|
+
workloadEnvironmentId: import("drizzle-orm/pg-core").PgColumn<{
|
|
2157
|
+
name: "workload_environment_id";
|
|
2158
|
+
tableName: "app_oauth_refresh_tokens";
|
|
2159
|
+
dataType: "string";
|
|
2160
|
+
columnType: "PgText";
|
|
2161
|
+
data: string;
|
|
2162
|
+
driverParam: string;
|
|
2163
|
+
notNull: false;
|
|
2164
|
+
hasDefault: false;
|
|
2165
|
+
isPrimaryKey: false;
|
|
2166
|
+
isAutoincrement: false;
|
|
2167
|
+
hasRuntimeDefault: false;
|
|
2168
|
+
enumValues: [string, ...string[]];
|
|
2169
|
+
baseColumn: never;
|
|
2170
|
+
identity: undefined;
|
|
2171
|
+
generated: undefined;
|
|
2172
|
+
}, {}, {}>;
|
|
2173
|
+
contractGeneration: import("drizzle-orm/pg-core").PgColumn<{
|
|
2174
|
+
name: "contract_generation";
|
|
2175
|
+
tableName: "app_oauth_refresh_tokens";
|
|
2176
|
+
dataType: "number";
|
|
2177
|
+
columnType: "PgInteger";
|
|
2178
|
+
data: number;
|
|
2179
|
+
driverParam: string | number;
|
|
2180
|
+
notNull: false;
|
|
2181
|
+
hasDefault: false;
|
|
2182
|
+
isPrimaryKey: false;
|
|
2183
|
+
isAutoincrement: false;
|
|
2184
|
+
hasRuntimeDefault: false;
|
|
2185
|
+
enumValues: undefined;
|
|
2186
|
+
baseColumn: never;
|
|
2187
|
+
identity: undefined;
|
|
2188
|
+
generated: undefined;
|
|
2189
|
+
}, {}, {}>;
|
|
2054
2190
|
status: import("drizzle-orm/pg-core").PgColumn<{
|
|
2055
2191
|
name: "status";
|
|
2056
2192
|
tableName: "app_oauth_refresh_tokens";
|
|
@@ -3007,6 +3143,40 @@ export declare const appSessionTokens: import("drizzle-orm/pg-core").PgTableWith
|
|
|
3007
3143
|
identity: undefined;
|
|
3008
3144
|
generated: undefined;
|
|
3009
3145
|
}, {}, {}>;
|
|
3146
|
+
workloadEnvironmentId: import("drizzle-orm/pg-core").PgColumn<{
|
|
3147
|
+
name: "workload_environment_id";
|
|
3148
|
+
tableName: "app_session_tokens";
|
|
3149
|
+
dataType: "string";
|
|
3150
|
+
columnType: "PgText";
|
|
3151
|
+
data: string;
|
|
3152
|
+
driverParam: string;
|
|
3153
|
+
notNull: false;
|
|
3154
|
+
hasDefault: false;
|
|
3155
|
+
isPrimaryKey: false;
|
|
3156
|
+
isAutoincrement: false;
|
|
3157
|
+
hasRuntimeDefault: false;
|
|
3158
|
+
enumValues: [string, ...string[]];
|
|
3159
|
+
baseColumn: never;
|
|
3160
|
+
identity: undefined;
|
|
3161
|
+
generated: undefined;
|
|
3162
|
+
}, {}, {}>;
|
|
3163
|
+
contractGeneration: import("drizzle-orm/pg-core").PgColumn<{
|
|
3164
|
+
name: "contract_generation";
|
|
3165
|
+
tableName: "app_session_tokens";
|
|
3166
|
+
dataType: "number";
|
|
3167
|
+
columnType: "PgInteger";
|
|
3168
|
+
data: number;
|
|
3169
|
+
driverParam: string | number;
|
|
3170
|
+
notNull: false;
|
|
3171
|
+
hasDefault: false;
|
|
3172
|
+
isPrimaryKey: false;
|
|
3173
|
+
isAutoincrement: false;
|
|
3174
|
+
hasRuntimeDefault: false;
|
|
3175
|
+
enumValues: undefined;
|
|
3176
|
+
baseColumn: never;
|
|
3177
|
+
identity: undefined;
|
|
3178
|
+
generated: undefined;
|
|
3179
|
+
}, {}, {}>;
|
|
3010
3180
|
jti: import("drizzle-orm/pg-core").PgColumn<{
|
|
3011
3181
|
name: "jti";
|
|
3012
3182
|
tableName: "app_session_tokens";
|
package/dist/schema.js
CHANGED
|
@@ -155,6 +155,8 @@ export const appInstallations = pgTable("app_installations", {
|
|
|
155
155
|
.notNull()
|
|
156
156
|
.references(() => apps.id, { onDelete: "cascade" }),
|
|
157
157
|
deploymentId: text("deployment_id").notNull(),
|
|
158
|
+
workloadEnvironmentId: text("workload_environment_id"),
|
|
159
|
+
contractGeneration: integer("contract_generation"),
|
|
158
160
|
releaseId: text("release_id")
|
|
159
161
|
.notNull()
|
|
160
162
|
.references(() => appReleases.id, { onDelete: "restrict" }),
|
|
@@ -182,6 +184,10 @@ export const appInstallations = pgTable("app_installations", {
|
|
|
182
184
|
index("idx_app_installations_app").on(table.appId, table.status),
|
|
183
185
|
index("idx_app_installations_deployment").on(table.deploymentId, table.status),
|
|
184
186
|
uniqueIndex("uidx_app_installations_deployment_app").on(table.deploymentId, table.appId),
|
|
187
|
+
uniqueIndex("uidx_app_installations_workload_environment_app")
|
|
188
|
+
.on(table.workloadEnvironmentId, table.appId)
|
|
189
|
+
.where(sql `${table.workloadEnvironmentId} IS NOT NULL`),
|
|
190
|
+
check("app_installations_managed_binding_complete", sql `(${table.workloadEnvironmentId} IS NULL AND ${table.contractGeneration} IS NULL) OR (${table.workloadEnvironmentId} IS NOT NULL AND ${table.contractGeneration} > 0)`),
|
|
185
191
|
// agent-quality: raw-sql reviewed -- owner: apps; identifier is a Drizzle-owned column and the literal prefix is static.
|
|
186
192
|
check("app_installations_namespace_reserved", sql `${table.namespace} LIKE 'app--%'`),
|
|
187
193
|
]);
|
|
@@ -206,6 +212,8 @@ export const appAccessCredentials = pgTable("app_access_credentials", {
|
|
|
206
212
|
.notNull()
|
|
207
213
|
.references(() => appInstallations.id, { onDelete: "cascade" }),
|
|
208
214
|
generation: integer("generation").notNull(),
|
|
215
|
+
workloadEnvironmentId: text("workload_environment_id"),
|
|
216
|
+
contractGeneration: integer("contract_generation"),
|
|
209
217
|
tokenMode: appAccessTokenModeEnum("token_mode").notNull().default("offline"),
|
|
210
218
|
credentialHash: text("credential_hash").notNull(),
|
|
211
219
|
encryptedMetadata: jsonb("encrypted_metadata").$type().notNull(),
|
|
@@ -218,6 +226,7 @@ export const appAccessCredentials = pgTable("app_access_credentials", {
|
|
|
218
226
|
}, (table) => [
|
|
219
227
|
index("idx_app_access_credentials_installation").on(table.installationId, table.status),
|
|
220
228
|
index("idx_app_access_credentials_generation").on(table.installationId, table.tokenMode, table.generation),
|
|
229
|
+
check("app_access_credentials_managed_binding_complete", sql `(${table.workloadEnvironmentId} IS NULL AND ${table.contractGeneration} IS NULL) OR (${table.workloadEnvironmentId} IS NOT NULL AND ${table.contractGeneration} > 0)`),
|
|
221
230
|
]);
|
|
222
231
|
export const appOAuthAuthorizationCodes = pgTable("app_oauth_authorization_codes", {
|
|
223
232
|
id: typeId("app_oauth_authorization_codes"),
|
|
@@ -231,6 +240,8 @@ export const appOAuthAuthorizationCodes = pgTable("app_oauth_authorization_codes
|
|
|
231
240
|
.notNull()
|
|
232
241
|
.references(() => appReleases.id, { onDelete: "cascade" }),
|
|
233
242
|
deploymentId: text("deployment_id").notNull(),
|
|
243
|
+
workloadEnvironmentId: text("workload_environment_id"),
|
|
244
|
+
contractGeneration: integer("contract_generation"),
|
|
234
245
|
codeHash: text("code_hash").notNull(),
|
|
235
246
|
stateHash: text("state_hash").notNull(),
|
|
236
247
|
redirectUri: text("redirect_uri").notNull(),
|
|
@@ -246,6 +257,7 @@ export const appOAuthAuthorizationCodes = pgTable("app_oauth_authorization_codes
|
|
|
246
257
|
}, (table) => [
|
|
247
258
|
uniqueIndex("uidx_app_oauth_codes_hash").on(table.codeHash),
|
|
248
259
|
index("idx_app_oauth_codes_installation").on(table.installationId, table.expiresAt),
|
|
260
|
+
check("app_oauth_codes_managed_binding_complete", sql `(${table.workloadEnvironmentId} IS NULL AND ${table.contractGeneration} IS NULL) OR (${table.workloadEnvironmentId} IS NOT NULL AND ${table.contractGeneration} > 0)`),
|
|
249
261
|
]);
|
|
250
262
|
export const appOAuthRefreshTokens = pgTable("app_oauth_refresh_tokens", {
|
|
251
263
|
id: typeId("app_oauth_refresh_tokens"),
|
|
@@ -254,6 +266,8 @@ export const appOAuthRefreshTokens = pgTable("app_oauth_refresh_tokens", {
|
|
|
254
266
|
.references(() => appInstallations.id, { onDelete: "cascade" }),
|
|
255
267
|
tokenHash: text("token_hash").notNull(),
|
|
256
268
|
generation: integer("generation").notNull(),
|
|
269
|
+
workloadEnvironmentId: text("workload_environment_id"),
|
|
270
|
+
contractGeneration: integer("contract_generation"),
|
|
257
271
|
status: appAccessCredentialStatusEnum("status").notNull().default("active"),
|
|
258
272
|
rotatedFromId: text("rotated_from_id"),
|
|
259
273
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
@@ -262,6 +276,7 @@ export const appOAuthRefreshTokens = pgTable("app_oauth_refresh_tokens", {
|
|
|
262
276
|
}, (table) => [
|
|
263
277
|
uniqueIndex("uidx_app_oauth_refresh_tokens_hash").on(table.tokenHash),
|
|
264
278
|
index("idx_app_oauth_refresh_tokens_installation").on(table.installationId, table.status),
|
|
279
|
+
check("app_oauth_refresh_tokens_managed_binding_complete", sql `(${table.workloadEnvironmentId} IS NULL AND ${table.contractGeneration} IS NULL) OR (${table.workloadEnvironmentId} IS NOT NULL AND ${table.contractGeneration} > 0)`),
|
|
265
280
|
]);
|
|
266
281
|
export const appInstallationSettings = pgTable("app_installation_settings", {
|
|
267
282
|
id: typeId("app_installation_settings"),
|
|
@@ -354,6 +369,8 @@ export const appSessionTokens = pgTable("app_session_tokens", {
|
|
|
354
369
|
.references(() => appInstallations.id, { onDelete: "cascade" }),
|
|
355
370
|
appId: text("app_id").notNull(),
|
|
356
371
|
deploymentId: text("deployment_id").notNull(),
|
|
372
|
+
workloadEnvironmentId: text("workload_environment_id"),
|
|
373
|
+
contractGeneration: integer("contract_generation"),
|
|
357
374
|
jti: text("jti").notNull(),
|
|
358
375
|
viewerId: text("viewer_id").notNull(),
|
|
359
376
|
entityType: text("entity_type"),
|
|
@@ -366,4 +383,5 @@ export const appSessionTokens = pgTable("app_session_tokens", {
|
|
|
366
383
|
}, (table) => [
|
|
367
384
|
uniqueIndex("uidx_app_session_tokens_jti").on(table.jti),
|
|
368
385
|
index("idx_app_session_tokens_installation").on(table.installationId, table.expiresAt),
|
|
386
|
+
check("app_session_tokens_managed_binding_complete", sql `(${table.workloadEnvironmentId} IS NULL AND ${table.contractGeneration} IS NULL) OR (${table.workloadEnvironmentId} IS NOT NULL AND ${table.contractGeneration} > 0)`),
|
|
369
387
|
]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
2
|
import type { createAppOAuthService } from "./oauth-service.js";
|
|
3
|
+
import type { ManagedAppInstallationAuthority } from "./runtime-port.js";
|
|
3
4
|
import { type AppSessionTokenEntity } from "./session-token.js";
|
|
4
5
|
type AppOAuthService = ReturnType<typeof createAppOAuthService>;
|
|
5
6
|
export interface AppSessionTokenServiceOptions {
|
|
@@ -7,6 +8,7 @@ export interface AppSessionTokenServiceOptions {
|
|
|
7
8
|
secret: string;
|
|
8
9
|
/** The deployment audience the tokens are bound to. */
|
|
9
10
|
deploymentId: string;
|
|
11
|
+
managedInstallation?: ManagedAppInstallationAuthority;
|
|
10
12
|
/** OAuth service supplying the online actor-token-exchange primitive. */
|
|
11
13
|
oauth: AppOAuthService;
|
|
12
14
|
ttlSeconds?: number;
|
|
@@ -15,6 +17,8 @@ export interface AppSessionTokenServiceOptions {
|
|
|
15
17
|
export interface IssueAppSessionTokenInput {
|
|
16
18
|
installationId: string;
|
|
17
19
|
viewerId: string;
|
|
20
|
+
/** Current host-authenticated viewer permissions. */
|
|
21
|
+
viewerScopes?: readonly string[];
|
|
18
22
|
entity?: AppSessionTokenEntity | null;
|
|
19
23
|
slot?: string | null;
|
|
20
24
|
}
|