@voyant-travel/apps 0.1.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 +29 -0
- package/dist/access-boundary.js +33 -0
- package/dist/api-runtime.d.ts +10 -0
- package/dist/api-runtime.js +8 -0
- package/dist/app-api-contracts.d.ts +51 -0
- package/dist/app-api-contracts.js +50 -0
- package/dist/app-api-routes.d.ts +26 -0
- package/dist/app-api-routes.js +133 -0
- package/dist/app-api-service.d.ts +1263 -0
- package/dist/app-api-service.js +231 -0
- package/dist/app-api-service.test.d.ts +1 -0
- package/dist/app-api-service.test.js +105 -0
- package/dist/compiler.d.ts +38 -0
- package/dist/compiler.js +118 -0
- package/dist/compiler.test.d.ts +1 -0
- package/dist/compiler.test.js +99 -0
- package/dist/consent.d.ts +15 -0
- package/dist/consent.js +50 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +80 -0
- package/dist/contracts.d.ts +263 -0
- package/dist/contracts.js +273 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/ingestion.d.ts +15 -0
- package/dist/ingestion.js +232 -0
- package/dist/ingestion.test.d.ts +1 -0
- package/dist/ingestion.test.js +47 -0
- package/dist/installation-reconciliation.d.ts +18 -0
- package/dist/installation-reconciliation.js +254 -0
- package/dist/installation-service.d.ts +351 -0
- package/dist/installation-service.js +328 -0
- package/dist/installation-state.d.ts +15 -0
- package/dist/installation-state.js +17 -0
- package/dist/installation-state.test.d.ts +1 -0
- package/dist/installation-state.test.js +38 -0
- package/dist/oauth-crypto.d.ts +8 -0
- package/dist/oauth-crypto.js +23 -0
- package/dist/oauth-crypto.test.d.ts +1 -0
- package/dist/oauth-crypto.test.js +11 -0
- package/dist/oauth-service.d.ts +65 -0
- package/dist/oauth-service.js +381 -0
- package/dist/oauth-service.test.d.ts +1 -0
- package/dist/oauth-service.test.js +8 -0
- package/dist/routes.d.ts +17 -0
- package/dist/routes.js +131 -0
- package/dist/schema.d.ts +2937 -0
- package/dist/schema.js +342 -0
- package/dist/service.d.ts +95 -0
- package/dist/service.js +172 -0
- package/dist/service.test.d.ts +1 -0
- package/dist/service.test.js +178 -0
- package/dist/test-fixtures.d.ts +80 -0
- package/dist/test-fixtures.js +78 -0
- package/dist/voyant.d.ts +2 -0
- package/dist/voyant.js +115 -0
- package/dist/webhook-delivery.d.ts +69 -0
- package/dist/webhook-delivery.js +262 -0
- package/migrations/20260717000100_app_registry_foundation.sql +87 -0
- package/migrations/20260717111938_breezy_karma.sql +136 -0
- package/migrations/20260717123000_app_webhook_delivery_health.sql +4 -0
- package/migrations/20260717143000_app_oauth_authorization.sql +47 -0
- package/migrations/meta/_journal.json +34 -0
- package/package.json +159 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { appRedirectUris, appReleaseArtifacts, appReleaseLocalizations, appReleases, apps, } from "./schema.js";
|
|
3
|
+
import { createAppsService } from "./service.js";
|
|
4
|
+
import { validManifest } from "./test-fixtures.js";
|
|
5
|
+
function postgresStub(implementation) {
|
|
6
|
+
const db = Object.create(null);
|
|
7
|
+
Object.assign(db, implementation);
|
|
8
|
+
if (!("transaction" in db)) {
|
|
9
|
+
Object.assign(db, {
|
|
10
|
+
transaction: async (callback) => callback(db),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return db;
|
|
14
|
+
}
|
|
15
|
+
function createRegistryDb() {
|
|
16
|
+
const rows = {
|
|
17
|
+
apps: [],
|
|
18
|
+
releases: [],
|
|
19
|
+
artifacts: [],
|
|
20
|
+
localizations: [],
|
|
21
|
+
redirectUris: [],
|
|
22
|
+
};
|
|
23
|
+
let appSequence = 0;
|
|
24
|
+
let releaseSequence = 0;
|
|
25
|
+
const db = postgresStub({
|
|
26
|
+
insert: (table) => ({
|
|
27
|
+
values: (value) => {
|
|
28
|
+
const values = Array.isArray(value) ? value : [value];
|
|
29
|
+
const insertRows = () => {
|
|
30
|
+
if (table === apps) {
|
|
31
|
+
const row = { id: `app_${++appSequence}`, ...values[0] };
|
|
32
|
+
if (rows.apps.some((existing) => existing.platformNamespace === row.platformNamespace)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
rows.apps.push(row);
|
|
36
|
+
return [row];
|
|
37
|
+
}
|
|
38
|
+
if (table === appReleases) {
|
|
39
|
+
const row = { id: `release_${++releaseSequence}`, ...values[0] };
|
|
40
|
+
const duplicate = rows.releases.find((existing) => existing.appId === row.appId &&
|
|
41
|
+
existing.manifestDigest === row.manifestDigest);
|
|
42
|
+
if (duplicate)
|
|
43
|
+
return [];
|
|
44
|
+
rows.releases.push(row);
|
|
45
|
+
return [row];
|
|
46
|
+
}
|
|
47
|
+
if (table === appReleaseArtifacts)
|
|
48
|
+
rows.artifacts.push(...values);
|
|
49
|
+
if (table === appReleaseLocalizations)
|
|
50
|
+
rows.localizations.push(...values);
|
|
51
|
+
if (table === appRedirectUris)
|
|
52
|
+
rows.redirectUris.push(...values);
|
|
53
|
+
return values;
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
onConflictDoNothing: () => ({ returning: async () => insertRows() }),
|
|
57
|
+
returning: async () => insertRows(),
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
select: () => ({
|
|
62
|
+
from: (table) => ({
|
|
63
|
+
where: () => ({
|
|
64
|
+
for: () => ({
|
|
65
|
+
limit: async () => (table === apps ? rows.apps.slice(0, 1) : []),
|
|
66
|
+
}),
|
|
67
|
+
limit: async () => (table === appReleases ? rows.releases.slice(0, 1) : []),
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
return { db, rows };
|
|
73
|
+
}
|
|
74
|
+
describe("apps service", () => {
|
|
75
|
+
it("assigns immutable platform app ids and reserved namespaces independent of names", async () => {
|
|
76
|
+
const { db, rows } = createRegistryDb();
|
|
77
|
+
const service = createAppsService();
|
|
78
|
+
const first = await service.createCustomApp(db, {
|
|
79
|
+
ownerId: "owner_1",
|
|
80
|
+
displayName: "Acme Sync",
|
|
81
|
+
slug: "acme-sync",
|
|
82
|
+
redirectUris: [],
|
|
83
|
+
createdBy: "user_1",
|
|
84
|
+
});
|
|
85
|
+
const second = await service.createCustomApp(db, {
|
|
86
|
+
ownerId: "owner_1",
|
|
87
|
+
displayName: "Acme Sync",
|
|
88
|
+
slug: "acme-sync",
|
|
89
|
+
redirectUris: [],
|
|
90
|
+
createdBy: "user_1",
|
|
91
|
+
});
|
|
92
|
+
expect(first.id).not.toBe(second.id);
|
|
93
|
+
expect(first.platformNamespace).toMatch(/^app--[a-f0-9]+$/);
|
|
94
|
+
expect(second.platformNamespace).toMatch(/^app--[a-f0-9]+$/);
|
|
95
|
+
expect(first.platformNamespace).not.toBe(second.platformNamespace);
|
|
96
|
+
expect(rows.apps).toHaveLength(2);
|
|
97
|
+
});
|
|
98
|
+
it("creates immutable releases and treats the same digest as idempotent", async () => {
|
|
99
|
+
const { db, rows } = createRegistryDb();
|
|
100
|
+
const service = createAppsService();
|
|
101
|
+
const app = await service.createCustomApp(db, {
|
|
102
|
+
ownerId: "owner_1",
|
|
103
|
+
displayName: "Acme Sync",
|
|
104
|
+
slug: "acme-sync",
|
|
105
|
+
redirectUris: [],
|
|
106
|
+
createdBy: "user_1",
|
|
107
|
+
});
|
|
108
|
+
const first = await service.releaseFromUpload(db, app.id, {
|
|
109
|
+
manifest: validManifest,
|
|
110
|
+
createdBy: "user_1",
|
|
111
|
+
provenance: { source: "test" },
|
|
112
|
+
});
|
|
113
|
+
const second = await service.releaseFromUpload(db, app.id, {
|
|
114
|
+
manifest: validManifest,
|
|
115
|
+
createdBy: "user_1",
|
|
116
|
+
provenance: { source: "test" },
|
|
117
|
+
});
|
|
118
|
+
expect(first.created).toBe(true);
|
|
119
|
+
expect(second.created).toBe(false);
|
|
120
|
+
expect(first.release.id).toBe(second.release.id);
|
|
121
|
+
expect(rows.releases).toHaveLength(1);
|
|
122
|
+
expect(rows.artifacts).toHaveLength(1);
|
|
123
|
+
expect(rows.localizations.length).toBeGreaterThan(0);
|
|
124
|
+
expect(Object.keys(service)).not.toContain("updateRelease");
|
|
125
|
+
});
|
|
126
|
+
it("rejects reusing a release version with different content", async () => {
|
|
127
|
+
const { db, rows } = createRegistryDb();
|
|
128
|
+
const service = createAppsService();
|
|
129
|
+
const app = await service.createCustomApp(db, {
|
|
130
|
+
ownerId: "owner_1",
|
|
131
|
+
displayName: "Acme Sync",
|
|
132
|
+
slug: "acme-sync",
|
|
133
|
+
redirectUris: [],
|
|
134
|
+
createdBy: "user_1",
|
|
135
|
+
});
|
|
136
|
+
await service.releaseFromUpload(db, app.id, {
|
|
137
|
+
manifest: validManifest,
|
|
138
|
+
createdBy: "user_1",
|
|
139
|
+
provenance: { source: "test" },
|
|
140
|
+
});
|
|
141
|
+
await expect(service.releaseFromUpload(db, app.id, {
|
|
142
|
+
manifest: {
|
|
143
|
+
...validManifest,
|
|
144
|
+
scopes: { requested: ["bookings:read", "invoices:read"], optional: [] },
|
|
145
|
+
},
|
|
146
|
+
createdBy: "user_1",
|
|
147
|
+
provenance: { source: "test" },
|
|
148
|
+
})).rejects.toMatchObject({ status: 409 });
|
|
149
|
+
expect(rows.releases).toHaveLength(1);
|
|
150
|
+
});
|
|
151
|
+
it("fetch ingestion stores only the validated snapshot", async () => {
|
|
152
|
+
const { db, rows } = createRegistryDb();
|
|
153
|
+
const fetcher = vi.fn(async () => new Response(JSON.stringify(validManifest), {
|
|
154
|
+
headers: { "content-type": "application/json" },
|
|
155
|
+
}));
|
|
156
|
+
const service = createAppsService({
|
|
157
|
+
fetch: fetcher,
|
|
158
|
+
resolveHost: async () => ["203.0.113.10"],
|
|
159
|
+
});
|
|
160
|
+
const app = await service.createCustomApp(db, {
|
|
161
|
+
ownerId: "owner_1",
|
|
162
|
+
displayName: "Acme Sync",
|
|
163
|
+
slug: "acme-sync",
|
|
164
|
+
redirectUris: [],
|
|
165
|
+
createdBy: "user_1",
|
|
166
|
+
});
|
|
167
|
+
await service.releaseFromFetch(db, app.id, {
|
|
168
|
+
manifestUrl: "https://app.example.com/manifest.json",
|
|
169
|
+
createdBy: "user_1",
|
|
170
|
+
});
|
|
171
|
+
expect(rows.releases[0]).toMatchObject({
|
|
172
|
+
appId: app.id,
|
|
173
|
+
manifestSchemaVersion: "voyant.app-manifest.v1",
|
|
174
|
+
});
|
|
175
|
+
expect(rows.releases[0]?.manifestSnapshot).not.toHaveProperty("scripts");
|
|
176
|
+
expect(rows.artifacts[0]?.provenance).toMatchObject({ source: "https-manifest" });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export declare const validManifest: {
|
|
2
|
+
schemaVersion: "voyant.app-manifest.v1";
|
|
3
|
+
releaseVersion: string;
|
|
4
|
+
apiCompatibility: {
|
|
5
|
+
min: string;
|
|
6
|
+
max: string;
|
|
7
|
+
};
|
|
8
|
+
scopes: {
|
|
9
|
+
requested: string[];
|
|
10
|
+
optional: string[];
|
|
11
|
+
};
|
|
12
|
+
admin: {
|
|
13
|
+
pages: {
|
|
14
|
+
key: string;
|
|
15
|
+
titleKey: string;
|
|
16
|
+
path: string;
|
|
17
|
+
entryUrl: string;
|
|
18
|
+
}[];
|
|
19
|
+
slotExtensions: {
|
|
20
|
+
key: string;
|
|
21
|
+
titleKey: string;
|
|
22
|
+
version: string;
|
|
23
|
+
extensionApi: string;
|
|
24
|
+
entryUrl: string;
|
|
25
|
+
slots: "booking.details.header"[];
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
webhooks: {
|
|
29
|
+
eventType: string;
|
|
30
|
+
eventVersion: string;
|
|
31
|
+
endpointUrl: string;
|
|
32
|
+
}[];
|
|
33
|
+
customFields: {
|
|
34
|
+
entityType: string;
|
|
35
|
+
key: string;
|
|
36
|
+
label: string;
|
|
37
|
+
fieldType: "text";
|
|
38
|
+
isRequired: false;
|
|
39
|
+
isSearchable: false;
|
|
40
|
+
isExportable: true;
|
|
41
|
+
isInvoiceable: false;
|
|
42
|
+
options: null;
|
|
43
|
+
dataClassification: "internal";
|
|
44
|
+
}[];
|
|
45
|
+
locales: {
|
|
46
|
+
default: string;
|
|
47
|
+
supported: string[];
|
|
48
|
+
host: {
|
|
49
|
+
"en-US": {
|
|
50
|
+
name: string;
|
|
51
|
+
summary: string;
|
|
52
|
+
navigation: {
|
|
53
|
+
settings: string;
|
|
54
|
+
};
|
|
55
|
+
extensions: {
|
|
56
|
+
"booking-panel": string;
|
|
57
|
+
};
|
|
58
|
+
setup: {};
|
|
59
|
+
};
|
|
60
|
+
"ro-RO": {
|
|
61
|
+
name: string;
|
|
62
|
+
summary: string;
|
|
63
|
+
navigation: {};
|
|
64
|
+
extensions: {};
|
|
65
|
+
setup: {};
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
urls: {
|
|
70
|
+
health: string;
|
|
71
|
+
launch: string;
|
|
72
|
+
privacy: string;
|
|
73
|
+
support: string;
|
|
74
|
+
};
|
|
75
|
+
data: {
|
|
76
|
+
classifications: "internal"[];
|
|
77
|
+
retention: string;
|
|
78
|
+
storesSecrets: false;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const validManifest = {
|
|
2
|
+
schemaVersion: "voyant.app-manifest.v1",
|
|
3
|
+
releaseVersion: "1.0.0",
|
|
4
|
+
apiCompatibility: { min: "2026-07-01", max: "2026-12-31" },
|
|
5
|
+
scopes: { requested: ["bookings:read"], optional: ["invoices:read"] },
|
|
6
|
+
admin: {
|
|
7
|
+
pages: [
|
|
8
|
+
{
|
|
9
|
+
key: "settings",
|
|
10
|
+
titleKey: "settings.title",
|
|
11
|
+
path: "/settings",
|
|
12
|
+
entryUrl: "https://app.example.com/admin/settings",
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
slotExtensions: [
|
|
16
|
+
{
|
|
17
|
+
key: "booking-panel",
|
|
18
|
+
titleKey: "booking.panel.title",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
extensionApi: "^1",
|
|
21
|
+
entryUrl: "https://app.example.com/admin/booking-panel",
|
|
22
|
+
slots: ["booking.details.header"],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
webhooks: [
|
|
27
|
+
{
|
|
28
|
+
eventType: "booking.created",
|
|
29
|
+
eventVersion: "1.0.0",
|
|
30
|
+
endpointUrl: "https://app.example.com/webhooks/voyant",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
customFields: [
|
|
34
|
+
{
|
|
35
|
+
entityType: "booking",
|
|
36
|
+
key: "external_id",
|
|
37
|
+
label: "External ID",
|
|
38
|
+
fieldType: "text",
|
|
39
|
+
isRequired: false,
|
|
40
|
+
isSearchable: false,
|
|
41
|
+
isExportable: true,
|
|
42
|
+
isInvoiceable: false,
|
|
43
|
+
options: null,
|
|
44
|
+
dataClassification: "internal",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
locales: {
|
|
48
|
+
default: "en-US",
|
|
49
|
+
supported: ["en-US", "ro-RO"],
|
|
50
|
+
host: {
|
|
51
|
+
"en-US": {
|
|
52
|
+
name: "Example App",
|
|
53
|
+
summary: "Synchronizes example records.",
|
|
54
|
+
navigation: { settings: "Example" },
|
|
55
|
+
extensions: { "booking-panel": "Example panel" },
|
|
56
|
+
setup: {},
|
|
57
|
+
},
|
|
58
|
+
"ro-RO": {
|
|
59
|
+
name: "Aplicatie Exemplu",
|
|
60
|
+
summary: "Sincronizeaza inregistrari exemplu.",
|
|
61
|
+
navigation: {},
|
|
62
|
+
extensions: {},
|
|
63
|
+
setup: {},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
urls: {
|
|
68
|
+
health: "https://app.example.com/health",
|
|
69
|
+
launch: "https://app.example.com/launch",
|
|
70
|
+
privacy: "https://app.example.com/privacy",
|
|
71
|
+
support: "https://app.example.com/support",
|
|
72
|
+
},
|
|
73
|
+
data: {
|
|
74
|
+
classifications: ["internal"],
|
|
75
|
+
retention: "Retained until the operator removes the app-owned records.",
|
|
76
|
+
storesSecrets: false,
|
|
77
|
+
},
|
|
78
|
+
};
|
package/dist/voyant.d.ts
ADDED
package/dist/voyant.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { defineModule } from "@voyant-travel/core/project";
|
|
2
|
+
const appInstallationLifecyclePayloadSchema = {
|
|
3
|
+
type: "object",
|
|
4
|
+
additionalProperties: false,
|
|
5
|
+
required: ["appId", "installationId", "deploymentId"],
|
|
6
|
+
properties: {
|
|
7
|
+
appId: { type: "string" },
|
|
8
|
+
installationId: { type: "string" },
|
|
9
|
+
deploymentId: { type: "string" },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export const appsVoyantModule = defineModule({
|
|
13
|
+
id: "@voyant-travel/apps",
|
|
14
|
+
packageName: "@voyant-travel/apps",
|
|
15
|
+
localId: "apps",
|
|
16
|
+
api: [
|
|
17
|
+
{
|
|
18
|
+
id: "@voyant-travel/apps#api.admin",
|
|
19
|
+
surface: "admin",
|
|
20
|
+
mount: "apps",
|
|
21
|
+
resource: "apps",
|
|
22
|
+
transactional: true,
|
|
23
|
+
runtime: {
|
|
24
|
+
entry: "@voyant-travel/apps/api-runtime",
|
|
25
|
+
export: "createAppsApiModule",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
id: "@voyant-travel/apps#schema",
|
|
32
|
+
source: "@voyant-travel/apps/schema",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
migrations: [
|
|
36
|
+
{
|
|
37
|
+
id: "@voyant-travel/apps#migrations",
|
|
38
|
+
source: "./migrations",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
events: [
|
|
42
|
+
{
|
|
43
|
+
id: "@voyant-travel/apps#event.installation-active",
|
|
44
|
+
eventType: "app.installation.active",
|
|
45
|
+
version: "1.0.0",
|
|
46
|
+
payloadSchema: appInstallationLifecyclePayloadSchema,
|
|
47
|
+
visibility: "internal",
|
|
48
|
+
audit: { sourceModule: "apps", category: "internal" },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "@voyant-travel/apps#event.installation-upgraded",
|
|
52
|
+
eventType: "app.installation.upgraded",
|
|
53
|
+
version: "1.0.0",
|
|
54
|
+
payloadSchema: appInstallationLifecyclePayloadSchema,
|
|
55
|
+
visibility: "internal",
|
|
56
|
+
audit: { sourceModule: "apps", category: "internal" },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "@voyant-travel/apps#event.installation-upgrade-pending",
|
|
60
|
+
eventType: "app.installation.upgrade_pending",
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
payloadSchema: appInstallationLifecyclePayloadSchema,
|
|
63
|
+
visibility: "internal",
|
|
64
|
+
audit: { sourceModule: "apps", category: "internal" },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "@voyant-travel/apps#event.installation-paused",
|
|
68
|
+
eventType: "app.installation.paused",
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
payloadSchema: appInstallationLifecyclePayloadSchema,
|
|
71
|
+
visibility: "internal",
|
|
72
|
+
audit: { sourceModule: "apps", category: "internal" },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "@voyant-travel/apps#event.installation-uninstalled",
|
|
76
|
+
eventType: "app.installation.uninstalled",
|
|
77
|
+
version: "1.0.0",
|
|
78
|
+
payloadSchema: appInstallationLifecyclePayloadSchema,
|
|
79
|
+
visibility: "internal",
|
|
80
|
+
audit: { sourceModule: "apps", category: "internal" },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
access: {
|
|
84
|
+
resources: [
|
|
85
|
+
{
|
|
86
|
+
id: "@voyant-travel/apps#access.apps",
|
|
87
|
+
resource: "apps",
|
|
88
|
+
label: "Apps",
|
|
89
|
+
description: "Administer app registrations and immutable app releases.",
|
|
90
|
+
actions: [
|
|
91
|
+
{
|
|
92
|
+
action: "read",
|
|
93
|
+
label: "View apps",
|
|
94
|
+
description: "View app registrations and releases.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
action: "write",
|
|
98
|
+
label: "Administer apps",
|
|
99
|
+
description: "Create custom app registrations and immutable app releases.",
|
|
100
|
+
sensitive: true,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
lifecycle: { uninstall: { default: "retain-data", purge: "not-supported" } },
|
|
107
|
+
meta: {
|
|
108
|
+
ownership: "package",
|
|
109
|
+
agentTools: {
|
|
110
|
+
posture: "not-applicable",
|
|
111
|
+
rationale: "The app registry is an authenticated admin API surface; this module does not expose agent Tools.",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
export default appsVoyantModule;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { EventEnvelope } from "@voyant-travel/core";
|
|
2
|
+
import { type ExternalWebhookEventContract, type WebhookDeliveryStore, type WebhookEnqueueOutcome, type WebhookSigningKey } from "@voyant-travel/webhook-delivery";
|
|
3
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
|
+
export interface AppWebhookDeliveryOptions {
|
|
5
|
+
contracts: readonly ExternalWebhookEventContract[];
|
|
6
|
+
resolveSigningKey(input: {
|
|
7
|
+
appId: string;
|
|
8
|
+
installationId: string;
|
|
9
|
+
}): Promise<WebhookSigningKey>;
|
|
10
|
+
terminalFailureThreshold?: number;
|
|
11
|
+
now?: () => Date;
|
|
12
|
+
}
|
|
13
|
+
export declare function createAppWebhookDeliveryStore(db: PostgresJsDatabase, options: Omit<AppWebhookDeliveryOptions, "contracts">): WebhookDeliveryStore;
|
|
14
|
+
export declare function createAppWebhookEventQueue(db: PostgresJsDatabase, options: AppWebhookDeliveryOptions): import("@voyant-travel/webhook-delivery").SelectedExternalWebhookQueue;
|
|
15
|
+
export declare function enqueueAppWebhookEvent(db: PostgresJsDatabase, event: EventEnvelope, options: AppWebhookDeliveryOptions): Promise<WebhookEnqueueOutcome[]>;
|
|
16
|
+
export declare function listAppWebhookHealth(db: PostgresJsDatabase, installationId: string): Promise<{
|
|
17
|
+
data: {
|
|
18
|
+
id: string;
|
|
19
|
+
installationId: string;
|
|
20
|
+
releaseId: string;
|
|
21
|
+
eventType: string;
|
|
22
|
+
eventVersion: string;
|
|
23
|
+
endpointUrl: string;
|
|
24
|
+
status: "active" | "inactive" | "failed";
|
|
25
|
+
externalSubscriptionId: string | null;
|
|
26
|
+
signingKeyId: string | null;
|
|
27
|
+
lastDeliveryAt: Date | null;
|
|
28
|
+
failureCount: number;
|
|
29
|
+
pausedAt: Date | null;
|
|
30
|
+
installedAt: Date;
|
|
31
|
+
deactivatedAt: Date | null;
|
|
32
|
+
}[];
|
|
33
|
+
}>;
|
|
34
|
+
export declare function replayAppWebhookDelivery(db: PostgresJsDatabase, input: {
|
|
35
|
+
deliveryId: string;
|
|
36
|
+
actorId: string;
|
|
37
|
+
signingKey: WebhookSigningKey;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
id: string;
|
|
40
|
+
sourceModule: string;
|
|
41
|
+
sourceEvent: string;
|
|
42
|
+
sourceEntityModule: string | null;
|
|
43
|
+
sourceEntityId: string | null;
|
|
44
|
+
subscriptionId: string | null;
|
|
45
|
+
targetUrl: string;
|
|
46
|
+
targetKind: string | null;
|
|
47
|
+
targetRef: string | null;
|
|
48
|
+
requestMethod: string;
|
|
49
|
+
requestHeaders: Record<string, string> | null;
|
|
50
|
+
requestBodyHash: string | null;
|
|
51
|
+
requestBodyExcerpt: string | null;
|
|
52
|
+
requestPayload: Record<string, unknown> | null;
|
|
53
|
+
deliveryContract: Record<string, unknown> | null;
|
|
54
|
+
responseStatus: number | null;
|
|
55
|
+
responseHeaders: Record<string, string> | null;
|
|
56
|
+
responseBodyExcerpt: string | null;
|
|
57
|
+
attemptNumber: number;
|
|
58
|
+
parentDeliveryId: string | null;
|
|
59
|
+
idempotencyKey: string | null;
|
|
60
|
+
status: "pending" | "failed" | "succeeded" | "in_flight" | "abandoned";
|
|
61
|
+
scheduledFor: Date | null;
|
|
62
|
+
startedAt: Date | null;
|
|
63
|
+
finishedAt: Date | null;
|
|
64
|
+
durationMs: number | null;
|
|
65
|
+
errorClass: "timeout" | "network" | "4xx" | "5xx" | "adapter_error" | "rate_limited" | null;
|
|
66
|
+
errorMessage: string | null;
|
|
67
|
+
createdAt: Date;
|
|
68
|
+
updatedAt: Date;
|
|
69
|
+
}>;
|