@ingram-tech/pulumi-ingram-cloud 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/README.md +133 -0
- package/dist/connection.d.ts +22 -0
- package/dist/connection.js +65 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +25 -0
- package/dist/resources.d.ts +137 -0
- package/dist/resources.js +478 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @ingram-tech/pulumi-ingram-cloud
|
|
2
|
+
|
|
3
|
+
Pulumi dynamic resources for the [Ingram Cloud](https://cloud.ingram.tech) `/v1`
|
|
4
|
+
API. Declare your tenant's IC configuration — blueprints, MCP servers, channels,
|
|
5
|
+
webhooks, BYOK model keys — as Pulumi resources instead of imperative
|
|
6
|
+
`register-*.ts` / `ensure-blueprint.ts` scripts, so it lives in state and a change
|
|
7
|
+
is a `pulumi up`.
|
|
8
|
+
|
|
9
|
+
The library is published from the `pulumi/` folder of the
|
|
10
|
+
[`cloud.ingram.tech`](https://github.com/ingram-technologies/cloud.ingram.tech)
|
|
11
|
+
repo so the wrapper tracks the `/v1` surface it wraps. The API version it targets
|
|
12
|
+
is pinned per library version (`IC_API_VERSION`).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add @ingram-tech/pulumi-ingram-cloud @pulumi/pulumi
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## How the repos interact
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
cloud.ingram.tech/pulumi/ THIS LIBRARY — knows nothing about any consumer
|
|
24
|
+
(Blueprint, McpServer, TelegramBot, WhatsAppConfig,
|
|
25
|
+
Webhook, ModelKey, Secret)
|
|
26
|
+
|
|
27
|
+
app repos (thornhill, …) declare their BLUEPRINTS — the `instructions` are
|
|
28
|
+
app-owned content (prompts), so they stay in the app
|
|
29
|
+
repo, sourced from the app's own spec module. The app's
|
|
30
|
+
Pulumi program imports this library + those specs.
|
|
31
|
+
|
|
32
|
+
infra repo (~/src/infra) declares tenant CHANNEL config (MCP, Telegram, WhatsApp,
|
|
33
|
+
webhook, model keys) alongside DNS/DB/Vercel, and can
|
|
34
|
+
read an app stack's outputs via pulumi.StackReference to
|
|
35
|
+
push IC-derived values (blueprint ids, webhook secret)
|
|
36
|
+
into Vercel env. No prompts ever live here.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The hard rule: **prompts never leave the app repo.** This library carries no
|
|
40
|
+
content; it only carries the machinery.
|
|
41
|
+
|
|
42
|
+
## Connection
|
|
43
|
+
|
|
44
|
+
Every resource takes `baseUrl` + `token` (the tenant-admin bearer). Resolve them
|
|
45
|
+
once with `connectionFromConfig()` and spread:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import * as ic from "@ingram-tech/pulumi-ingram-cloud";
|
|
49
|
+
|
|
50
|
+
const conn = ic.connectionFromConfig(); // ingram-cloud:token (secret) / :baseUrl
|
|
51
|
+
// or env INGRAM_CLOUD_TOKEN|CLOUD_API_KEY
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Set the token as a stack secret:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pulumi config set --secret ingram-cloud:token tha_live_…
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Resources
|
|
61
|
+
|
|
62
|
+
### IcBlueprint (declare in the APP repo)
|
|
63
|
+
|
|
64
|
+
Create-or-adopt by name → publish a new immutable version **only when the content
|
|
65
|
+
changed** → roll it out. Mirrors the old `ensure-blueprint.ts` idempotency, so the
|
|
66
|
+
first `pulumi up` after switching from a script **adopts** the existing blueprint
|
|
67
|
+
(matched by name) rather than recreating it.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { BLUEPRINT_SPECS } from "../src/lib/cloud/blueprint-spec"; // app-owned
|
|
71
|
+
|
|
72
|
+
const curator = new ic.IcBlueprint("curator", {
|
|
73
|
+
...conn,
|
|
74
|
+
name: BLUEPRINT_SPECS.curator.name,
|
|
75
|
+
instructions: BLUEPRINT_SPECS.curator.instructions,
|
|
76
|
+
model: BLUEPRINT_SPECS.curator.model,
|
|
77
|
+
autoMemory: BLUEPRINT_SPECS.curator.auto_memory,
|
|
78
|
+
// variables: [...], enabledHostedTools: [...], rolloutPercent: 100,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const curatorBlueprintId = curator.blueprintId;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> Attaching **existing** principals to a blueprint is a one-time fleet backfill —
|
|
85
|
+
> keep it as the app's migration script. New principals attach at birth in app code.
|
|
86
|
+
> Pulumi does not own per-user runtime state.
|
|
87
|
+
|
|
88
|
+
### IcMcpServer, IcTelegramBot, IcWhatsAppConfig, IcWebhook, IcModelKey (declare in INFRA)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
new ic.IcMcpServer("thornhill-mcp", {
|
|
92
|
+
...conn, serverName: "thornhill", url: `${APP_URL}/api/mcp`,
|
|
93
|
+
authKind: "static", secret: mcpSecret,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
new ic.IcTelegramBot("thornhill-telegram", { ...conn, botToken });
|
|
97
|
+
|
|
98
|
+
const hook = new ic.IcWebhook("thornhill-events", {
|
|
99
|
+
...conn, url: `${APP_URL}/api/ic/events`,
|
|
100
|
+
events: ["run.completed", "approval.required", /* … */],
|
|
101
|
+
});
|
|
102
|
+
export const webhookSigningSecret = hook.secret; // whsec_… (create-only secret output)
|
|
103
|
+
|
|
104
|
+
new ic.IcModelKey("anthropic", { ...conn, provider: "anthropic", apiKey });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`IcWebhook.secret` is the `whsec_…` signing secret IC returns exactly once — it
|
|
108
|
+
becomes a secret Pulumi output, so the infra stack can flow it straight into Vercel
|
|
109
|
+
env with no copy-paste.
|
|
110
|
+
|
|
111
|
+
### IcSecret (IC-hosted apps only)
|
|
112
|
+
|
|
113
|
+
`PUT /v1/apps/{appId}/env` — one resource per secret name. Only relevant if your
|
|
114
|
+
tenant runs an IC-hosted `app`.
|
|
115
|
+
|
|
116
|
+
## Importing resources a script already created
|
|
117
|
+
|
|
118
|
+
Blueprints and webhooks implement `read`, so you can adopt pre-existing ones:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pulumi import 'ingram-cloud:index:IcBlueprint' curator bp_123…
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
(Blueprints also self-adopt by name on first `create`, so an explicit import is
|
|
125
|
+
usually unnecessary — a plain `pulumi up` will reconcile the live blueprint.)
|
|
126
|
+
|
|
127
|
+
## Notes
|
|
128
|
+
|
|
129
|
+
- Built as CommonJS to match the Pulumi Node.js runtime and the infra stack's
|
|
130
|
+
`module: commonjs` tsconfig.
|
|
131
|
+
- `token` and every credential input are marked `additionalSecretOutputs`, so they
|
|
132
|
+
are encrypted in Pulumi state.
|
|
133
|
+
- CRUD runs inline in the Pulumi process over the global `fetch` (Node ≥18).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection helper — resolves the IC `{ baseUrl, token }` pair once so a Pulumi
|
|
3
|
+
* program can spread it into every resource:
|
|
4
|
+
*
|
|
5
|
+
* const conn = connectionFromConfig();
|
|
6
|
+
* new ic.IcBlueprint("assistant", { ...conn, name: "…", instructions: "…" });
|
|
7
|
+
*
|
|
8
|
+
* Resolution order (first hit wins):
|
|
9
|
+
* baseUrl: `ingram-cloud:baseUrl` config → IC_BASE_URL → CLOUD_BASE_URL → prod
|
|
10
|
+
* token: `ingram-cloud:token` secret config → INGRAM_CLOUD_TOKEN → CLOUD_API_KEY
|
|
11
|
+
*
|
|
12
|
+
* The env fallbacks bridge the two naming conventions already in the fleet
|
|
13
|
+
* (thornhill's INGRAM_CLOUD_TOKEN, integrain's CLOUD_API_KEY). The returned
|
|
14
|
+
* token is always a secret Output so it stays encrypted in state.
|
|
15
|
+
*/
|
|
16
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
17
|
+
export declare const DEFAULT_BASE_URL = "https://api.cloud.ingram.tech";
|
|
18
|
+
export interface IcConnection {
|
|
19
|
+
baseUrl: pulumi.Input<string>;
|
|
20
|
+
token: pulumi.Input<string>;
|
|
21
|
+
}
|
|
22
|
+
export declare function connectionFromConfig(cfg?: pulumi.Config): IcConnection;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_BASE_URL = void 0;
|
|
37
|
+
exports.connectionFromConfig = connectionFromConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Connection helper — resolves the IC `{ baseUrl, token }` pair once so a Pulumi
|
|
40
|
+
* program can spread it into every resource:
|
|
41
|
+
*
|
|
42
|
+
* const conn = connectionFromConfig();
|
|
43
|
+
* new ic.IcBlueprint("assistant", { ...conn, name: "…", instructions: "…" });
|
|
44
|
+
*
|
|
45
|
+
* Resolution order (first hit wins):
|
|
46
|
+
* baseUrl: `ingram-cloud:baseUrl` config → IC_BASE_URL → CLOUD_BASE_URL → prod
|
|
47
|
+
* token: `ingram-cloud:token` secret config → INGRAM_CLOUD_TOKEN → CLOUD_API_KEY
|
|
48
|
+
*
|
|
49
|
+
* The env fallbacks bridge the two naming conventions already in the fleet
|
|
50
|
+
* (thornhill's INGRAM_CLOUD_TOKEN, integrain's CLOUD_API_KEY). The returned
|
|
51
|
+
* token is always a secret Output so it stays encrypted in state.
|
|
52
|
+
*/
|
|
53
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
54
|
+
exports.DEFAULT_BASE_URL = "https://api.cloud.ingram.tech";
|
|
55
|
+
function connectionFromConfig(cfg) {
|
|
56
|
+
const c = cfg ?? new pulumi.Config("ingram-cloud");
|
|
57
|
+
const baseUrl = c.get("baseUrl") ??
|
|
58
|
+
process.env.IC_BASE_URL ??
|
|
59
|
+
process.env.CLOUD_BASE_URL ??
|
|
60
|
+
exports.DEFAULT_BASE_URL;
|
|
61
|
+
const tokenFromCfg = c.getSecret("token");
|
|
62
|
+
const token = tokenFromCfg ??
|
|
63
|
+
pulumi.secret(process.env.INGRAM_CLOUD_TOKEN ?? process.env.CLOUD_API_KEY ?? "");
|
|
64
|
+
return { baseUrl, token };
|
|
65
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ingram-tech/pulumi-ingram-cloud
|
|
3
|
+
*
|
|
4
|
+
* Pulumi dynamic resources for the Ingram Cloud /v1 API. See README.md for the
|
|
5
|
+
* repo-interaction model (library here, blueprints declared in app repos,
|
|
6
|
+
* channel/tenant config in the infra repo).
|
|
7
|
+
*/
|
|
8
|
+
export * from "./resources";
|
|
9
|
+
export * from "./connection";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* @ingram-tech/pulumi-ingram-cloud
|
|
19
|
+
*
|
|
20
|
+
* Pulumi dynamic resources for the Ingram Cloud /v1 API. See README.md for the
|
|
21
|
+
* repo-interaction model (library here, blueprints declared in app repos,
|
|
22
|
+
* channel/tenant config in the infra repo).
|
|
23
|
+
*/
|
|
24
|
+
__exportStar(require("./resources"), exports);
|
|
25
|
+
__exportStar(require("./connection"), exports);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingram Cloud /v1 tenant configuration as Pulumi dynamic resources.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the per-app `scripts/register-*.ts` / `ensure-blueprint.ts` one-shots
|
|
5
|
+
* with declarative resources, so IC wiring is a `pulumi up` and lives in state
|
|
6
|
+
* instead of drifting in someone's shell history.
|
|
7
|
+
*
|
|
8
|
+
* Each resource talks to the IC REST API. `token` is the tenant-admin bearer;
|
|
9
|
+
* keep it (and any credential input) a stack secret — it's marked as an
|
|
10
|
+
* additional secret output so it's encrypted in state.
|
|
11
|
+
*
|
|
12
|
+
* Where each resource is meant to be declared:
|
|
13
|
+
* - **Blueprints** (`IcBlueprint`) — the `instructions` are app-owned *content*,
|
|
14
|
+
* so declare these in the APP repo's Pulumi program, sourced from the app's
|
|
15
|
+
* own spec module. Never copy prompts into the infra repo.
|
|
16
|
+
* - **Channels / tenant config** (`IcMcpServer`, `IcTelegramBot`,
|
|
17
|
+
* `IcWhatsAppConfig`, `IcWebhook`, `IcModelKey`) — tenant-level wiring;
|
|
18
|
+
* declare in the infra repo alongside DNS/DB/Vercel.
|
|
19
|
+
* - **App env secrets** (`IcSecret`) — only relevant to IC-hosted `apps`.
|
|
20
|
+
*
|
|
21
|
+
* Not modelled here, on purpose:
|
|
22
|
+
* - **Per-end-user actions** — creating users, binding channels, pushing
|
|
23
|
+
* per-user connection tokens, minting user tokens — happen at app runtime.
|
|
24
|
+
* - **Attaching existing principals to a blueprint** — a one-time backfill that
|
|
25
|
+
* mutates the live fleet; keep it as the app's migration script. New
|
|
26
|
+
* principals attach at birth in app code.
|
|
27
|
+
* - **Slack app factory** — config-token rotation is stateful and app-driven.
|
|
28
|
+
*/
|
|
29
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
30
|
+
/**
|
|
31
|
+
* The /v1 API version this library targets. Bumping the IC API contract is a
|
|
32
|
+
* library version bump — consumers pin behaviour by pinning this package.
|
|
33
|
+
*/
|
|
34
|
+
export declare const IC_API_VERSION = "2026-05-01";
|
|
35
|
+
export interface BlueprintVariable {
|
|
36
|
+
name: string;
|
|
37
|
+
default?: string | null;
|
|
38
|
+
description?: string | null;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface IcBlueprintArgs {
|
|
42
|
+
baseUrl: pulumi.Input<string>;
|
|
43
|
+
token: pulumi.Input<string>;
|
|
44
|
+
/** Tenant-unique blueprint name (the create/adopt key). */
|
|
45
|
+
name: pulumi.Input<string>;
|
|
46
|
+
/** The persona/instruction template (may contain `{{ variables }}`). */
|
|
47
|
+
instructions?: pulumi.Input<string>;
|
|
48
|
+
model?: pulumi.Input<string>;
|
|
49
|
+
enabledHostedTools?: pulumi.Input<string[]>;
|
|
50
|
+
autoMemory?: pulumi.Input<boolean>;
|
|
51
|
+
variables?: pulumi.Input<BlueprintVariable[]>;
|
|
52
|
+
/** Release note recorded when a new version is published. */
|
|
53
|
+
publishNote?: pulumi.Input<string>;
|
|
54
|
+
/** Rollout percentage for the published version (default 100). */
|
|
55
|
+
rolloutPercent?: pulumi.Input<number>;
|
|
56
|
+
}
|
|
57
|
+
export declare class IcBlueprint extends pulumi.dynamic.Resource {
|
|
58
|
+
/** The `bp_…` id (same as `.id`, exposed for convenience). */
|
|
59
|
+
readonly blueprintId: pulumi.Output<string>;
|
|
60
|
+
/** The version number currently rolled out. */
|
|
61
|
+
readonly activeVersion: pulumi.Output<number>;
|
|
62
|
+
constructor(name: string, args: IcBlueprintArgs, opts?: pulumi.CustomResourceOptions);
|
|
63
|
+
}
|
|
64
|
+
export interface IcMcpServerArgs {
|
|
65
|
+
baseUrl: pulumi.Input<string>;
|
|
66
|
+
token: pulumi.Input<string>;
|
|
67
|
+
/** Registry name — the `{name}` in `/v1/tenant/mcp/{name}`. */
|
|
68
|
+
serverName: pulumi.Input<string>;
|
|
69
|
+
url: pulumi.Input<string>;
|
|
70
|
+
authKind?: pulumi.Input<string>;
|
|
71
|
+
authProvider?: pulumi.Input<string>;
|
|
72
|
+
secret?: pulumi.Input<string>;
|
|
73
|
+
}
|
|
74
|
+
export declare class IcMcpServer extends pulumi.dynamic.Resource {
|
|
75
|
+
readonly toolsDiscovered: pulumi.Output<number>;
|
|
76
|
+
readonly discoveryError: pulumi.Output<string | undefined>;
|
|
77
|
+
constructor(name: string, args: IcMcpServerArgs, opts?: pulumi.CustomResourceOptions);
|
|
78
|
+
}
|
|
79
|
+
export interface IcTelegramBotArgs {
|
|
80
|
+
baseUrl: pulumi.Input<string>;
|
|
81
|
+
token: pulumi.Input<string>;
|
|
82
|
+
botToken: pulumi.Input<string>;
|
|
83
|
+
}
|
|
84
|
+
export declare class IcTelegramBot extends pulumi.dynamic.Resource {
|
|
85
|
+
readonly botUsername: pulumi.Output<string>;
|
|
86
|
+
readonly webhookUrl: pulumi.Output<string>;
|
|
87
|
+
constructor(name: string, args: IcTelegramBotArgs, opts?: pulumi.CustomResourceOptions);
|
|
88
|
+
}
|
|
89
|
+
export interface IcWhatsAppConfigArgs {
|
|
90
|
+
baseUrl: pulumi.Input<string>;
|
|
91
|
+
token: pulumi.Input<string>;
|
|
92
|
+
phoneNumberId: pulumi.Input<string>;
|
|
93
|
+
accessToken: pulumi.Input<string>;
|
|
94
|
+
appSecret?: pulumi.Input<string>;
|
|
95
|
+
verifyToken?: pulumi.Input<string>;
|
|
96
|
+
businessId?: pulumi.Input<string>;
|
|
97
|
+
}
|
|
98
|
+
export declare class IcWhatsAppConfig extends pulumi.dynamic.Resource {
|
|
99
|
+
readonly displayPhoneNumber: pulumi.Output<string>;
|
|
100
|
+
readonly webhookUrl: pulumi.Output<string>;
|
|
101
|
+
readonly verifyToken: pulumi.Output<string>;
|
|
102
|
+
constructor(name: string, args: IcWhatsAppConfigArgs, opts?: pulumi.CustomResourceOptions);
|
|
103
|
+
}
|
|
104
|
+
export interface IcWebhookArgs {
|
|
105
|
+
baseUrl: pulumi.Input<string>;
|
|
106
|
+
token: pulumi.Input<string>;
|
|
107
|
+
url: pulumi.Input<string>;
|
|
108
|
+
events: pulumi.Input<string[]>;
|
|
109
|
+
}
|
|
110
|
+
export declare class IcWebhook extends pulumi.dynamic.Resource {
|
|
111
|
+
/** The signing secret — present only when Pulumi created the webhook. */
|
|
112
|
+
readonly secret: pulumi.Output<string | undefined>;
|
|
113
|
+
constructor(name: string, args: IcWebhookArgs, opts?: pulumi.CustomResourceOptions);
|
|
114
|
+
}
|
|
115
|
+
export interface IcModelKeyArgs {
|
|
116
|
+
baseUrl: pulumi.Input<string>;
|
|
117
|
+
token: pulumi.Input<string>;
|
|
118
|
+
/** Provider id, e.g. `openai`, `anthropic`. */
|
|
119
|
+
provider: pulumi.Input<string>;
|
|
120
|
+
apiKey: pulumi.Input<string>;
|
|
121
|
+
/** Optional override base URL (OpenAI-compatible backends). */
|
|
122
|
+
providerBaseUrl?: pulumi.Input<string>;
|
|
123
|
+
}
|
|
124
|
+
export declare class IcModelKey extends pulumi.dynamic.Resource {
|
|
125
|
+
constructor(name: string, args: IcModelKeyArgs, opts?: pulumi.CustomResourceOptions);
|
|
126
|
+
}
|
|
127
|
+
export interface IcSecretArgs {
|
|
128
|
+
baseUrl: pulumi.Input<string>;
|
|
129
|
+
token: pulumi.Input<string>;
|
|
130
|
+
/** The IC-hosted app id (`app_…`) this secret belongs to. */
|
|
131
|
+
appId: pulumi.Input<string>;
|
|
132
|
+
name: pulumi.Input<string>;
|
|
133
|
+
value: pulumi.Input<string>;
|
|
134
|
+
}
|
|
135
|
+
export declare class IcSecret extends pulumi.dynamic.Resource {
|
|
136
|
+
constructor(name: string, args: IcSecretArgs, opts?: pulumi.CustomResourceOptions);
|
|
137
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.IcSecret = exports.IcModelKey = exports.IcWebhook = exports.IcWhatsAppConfig = exports.IcTelegramBot = exports.IcMcpServer = exports.IcBlueprint = exports.IC_API_VERSION = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* Ingram Cloud /v1 tenant configuration as Pulumi dynamic resources.
|
|
39
|
+
*
|
|
40
|
+
* Replaces the per-app `scripts/register-*.ts` / `ensure-blueprint.ts` one-shots
|
|
41
|
+
* with declarative resources, so IC wiring is a `pulumi up` and lives in state
|
|
42
|
+
* instead of drifting in someone's shell history.
|
|
43
|
+
*
|
|
44
|
+
* Each resource talks to the IC REST API. `token` is the tenant-admin bearer;
|
|
45
|
+
* keep it (and any credential input) a stack secret — it's marked as an
|
|
46
|
+
* additional secret output so it's encrypted in state.
|
|
47
|
+
*
|
|
48
|
+
* Where each resource is meant to be declared:
|
|
49
|
+
* - **Blueprints** (`IcBlueprint`) — the `instructions` are app-owned *content*,
|
|
50
|
+
* so declare these in the APP repo's Pulumi program, sourced from the app's
|
|
51
|
+
* own spec module. Never copy prompts into the infra repo.
|
|
52
|
+
* - **Channels / tenant config** (`IcMcpServer`, `IcTelegramBot`,
|
|
53
|
+
* `IcWhatsAppConfig`, `IcWebhook`, `IcModelKey`) — tenant-level wiring;
|
|
54
|
+
* declare in the infra repo alongside DNS/DB/Vercel.
|
|
55
|
+
* - **App env secrets** (`IcSecret`) — only relevant to IC-hosted `apps`.
|
|
56
|
+
*
|
|
57
|
+
* Not modelled here, on purpose:
|
|
58
|
+
* - **Per-end-user actions** — creating users, binding channels, pushing
|
|
59
|
+
* per-user connection tokens, minting user tokens — happen at app runtime.
|
|
60
|
+
* - **Attaching existing principals to a blueprint** — a one-time backfill that
|
|
61
|
+
* mutates the live fleet; keep it as the app's migration script. New
|
|
62
|
+
* principals attach at birth in app code.
|
|
63
|
+
* - **Slack app factory** — config-token rotation is stateful and app-driven.
|
|
64
|
+
*/
|
|
65
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
66
|
+
/**
|
|
67
|
+
* The /v1 API version this library targets. Bumping the IC API contract is a
|
|
68
|
+
* library version bump — consumers pin behaviour by pinning this package.
|
|
69
|
+
*/
|
|
70
|
+
exports.IC_API_VERSION = "2026-05-01";
|
|
71
|
+
// Shared IC REST call. Module-scope so Pulumi's closure serialization captures
|
|
72
|
+
// it into each provider's methods; uses only the global `fetch` (Node ≥18).
|
|
73
|
+
async function icRequest(baseUrl, token, method, path, body, idempotencyKey) {
|
|
74
|
+
const headers = {
|
|
75
|
+
Authorization: `Bearer ${token}`,
|
|
76
|
+
"IC-Api-Version": exports.IC_API_VERSION,
|
|
77
|
+
};
|
|
78
|
+
if (body !== undefined)
|
|
79
|
+
headers["Content-Type"] = "application/json";
|
|
80
|
+
if (idempotencyKey)
|
|
81
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
82
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
86
|
+
});
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
throw new Error(`IC ${method} ${path} → ${res.status}: ${text.slice(0, 400)}`);
|
|
90
|
+
}
|
|
91
|
+
return text ? JSON.parse(text) : {};
|
|
92
|
+
}
|
|
93
|
+
// The draft body IC's create/patch expect (snake_case wire shape).
|
|
94
|
+
function blueprintBody(i) {
|
|
95
|
+
return {
|
|
96
|
+
name: i.name,
|
|
97
|
+
instructions: i.instructions ?? null,
|
|
98
|
+
model: i.model ?? null,
|
|
99
|
+
enabled_hosted_tools: i.enabledHostedTools ?? [],
|
|
100
|
+
auto_memory: i.autoMemory ?? null,
|
|
101
|
+
variables: i.variables ?? [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Stable signature of the *content* that warrants a new published version
|
|
105
|
+
// (name and rollout% deliberately excluded — a rename/restage is not new content).
|
|
106
|
+
function blueprintContentSig(i) {
|
|
107
|
+
return JSON.stringify({
|
|
108
|
+
instructions: i.instructions ?? null,
|
|
109
|
+
model: i.model ?? null,
|
|
110
|
+
tools: [...(i.enabledHostedTools ?? [])].sort(),
|
|
111
|
+
auto_memory: i.autoMemory ?? null,
|
|
112
|
+
variables: i.variables ?? [],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function findBlueprintByName(baseUrl, token, name) {
|
|
116
|
+
const res = await icRequest(baseUrl, token, "GET", "/v1/blueprints");
|
|
117
|
+
return (res.data ?? []).find((b) => b.name === name) ?? null;
|
|
118
|
+
}
|
|
119
|
+
// Signature of the snapshot currently published as the active version, or null
|
|
120
|
+
// if nothing is published yet.
|
|
121
|
+
async function activeSnapshotSig(baseUrl, token, id, activeVersion) {
|
|
122
|
+
if (!activeVersion)
|
|
123
|
+
return null;
|
|
124
|
+
const res = await icRequest(baseUrl, token, "GET", `/v1/blueprints/${id}/versions`);
|
|
125
|
+
const v = (res.data ?? []).find((x) => x.version === activeVersion);
|
|
126
|
+
if (!v)
|
|
127
|
+
return null;
|
|
128
|
+
const s = v.snapshot ?? {};
|
|
129
|
+
return blueprintContentSig({
|
|
130
|
+
instructions: s.instructions,
|
|
131
|
+
model: s.model,
|
|
132
|
+
enabledHostedTools: s.enabled_hosted_tools,
|
|
133
|
+
autoMemory: s.auto_memory,
|
|
134
|
+
variables: s.variables,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Ensure the live blueprint matches desired: adopt-or-create, publish iff the
|
|
138
|
+
// content drifted from the active version, then roll out at `percent`.
|
|
139
|
+
async function reconcileBlueprint(i, knownId, priorSig) {
|
|
140
|
+
let id;
|
|
141
|
+
if (!knownId) {
|
|
142
|
+
const existing = await findBlueprintByName(i.baseUrl, i.token, i.name);
|
|
143
|
+
if (existing) {
|
|
144
|
+
id = existing.id;
|
|
145
|
+
await icRequest(i.baseUrl, i.token, "PATCH", `/v1/blueprints/${id}`, blueprintBody(i));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// No idempotency key: Pulumi calls create once and tracks the id in
|
|
149
|
+
// state, and the adopt-by-name branch above covers re-runs and the
|
|
150
|
+
// script→Pulumi migration. Keying on the name would replay a stale
|
|
151
|
+
// (possibly archived) create after a destroy+recreate within the 24h
|
|
152
|
+
// idempotency window.
|
|
153
|
+
const bp = await icRequest(i.baseUrl, i.token, "POST", "/v1/blueprints", blueprintBody(i));
|
|
154
|
+
id = bp.id;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Known id (update path): push the draft to the desired shape.
|
|
159
|
+
id = knownId;
|
|
160
|
+
await icRequest(i.baseUrl, i.token, "PATCH", `/v1/blueprints/${id}`, blueprintBody(i));
|
|
161
|
+
}
|
|
162
|
+
const bp = await icRequest(i.baseUrl, i.token, "GET", `/v1/blueprints/${id}`);
|
|
163
|
+
const desiredSig = blueprintContentSig(i);
|
|
164
|
+
// On the create path we don't know the prior sig — compare to what's actually
|
|
165
|
+
// published so adopting a script-made blueprint doesn't churn a needless version.
|
|
166
|
+
const liveSig = priorSig ?? (await activeSnapshotSig(i.baseUrl, i.token, id, bp.active_version));
|
|
167
|
+
let version = bp.active_version ?? 0;
|
|
168
|
+
if (!bp.active_version || liveSig !== desiredSig) {
|
|
169
|
+
// Publishing snapshots the draft as the next immutable version and returns
|
|
170
|
+
// its number. Only the *first* publish auto-activates; later ones go live
|
|
171
|
+
// only via the rollout below — so take the version from the publish reply,
|
|
172
|
+
// not from active_version (which hasn't advanced yet).
|
|
173
|
+
const pub = await icRequest(i.baseUrl, i.token, "POST", `/v1/blueprints/${id}/versions`, {
|
|
174
|
+
note: i.publishNote ?? null,
|
|
175
|
+
});
|
|
176
|
+
version = pub.version;
|
|
177
|
+
}
|
|
178
|
+
// Always assert the rollout: it activates a freshly published version and is
|
|
179
|
+
// an idempotent no-op when `version` is already the active one at `percent`.
|
|
180
|
+
const percent = i.rolloutPercent ?? 100;
|
|
181
|
+
await icRequest(i.baseUrl, i.token, "POST", `/v1/blueprints/${id}/rollout`, { version, percent });
|
|
182
|
+
return {
|
|
183
|
+
...i,
|
|
184
|
+
blueprintId: id,
|
|
185
|
+
contentSig: desiredSig,
|
|
186
|
+
activeVersion: version,
|
|
187
|
+
rolloutPercent: percent,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const blueprintProvider = {
|
|
191
|
+
async create(i) {
|
|
192
|
+
const outs = await reconcileBlueprint(i);
|
|
193
|
+
return { id: outs.blueprintId, outs };
|
|
194
|
+
},
|
|
195
|
+
async update(id, olds, news) {
|
|
196
|
+
const outs = await reconcileBlueprint(news, id, olds.contentSig);
|
|
197
|
+
return { outs };
|
|
198
|
+
},
|
|
199
|
+
async delete(id, props) {
|
|
200
|
+
try {
|
|
201
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/blueprints/${id}`);
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
throw new Error(`Blueprint ${id} could not be archived (it likely still has users attached). ` +
|
|
205
|
+
`Detach its users first, or set { retainOnDelete: true } on the resource. ` +
|
|
206
|
+
`Original error: ${e}`);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
async diff(_id, olds, news) {
|
|
210
|
+
const contentChanged = blueprintContentSig(olds) !== blueprintContentSig(news);
|
|
211
|
+
const nameChanged = olds.name !== news.name;
|
|
212
|
+
const rolloutChanged = (olds.rolloutPercent ?? 100) !== (news.rolloutPercent ?? 100);
|
|
213
|
+
return { changes: contentChanged || nameChanged || rolloutChanged };
|
|
214
|
+
},
|
|
215
|
+
// Adopt a blueprint a script already created (`pulumi import <id>`).
|
|
216
|
+
async read(id, props) {
|
|
217
|
+
const bp = await icRequest(props.baseUrl, props.token, "GET", `/v1/blueprints/${id}`);
|
|
218
|
+
if (!bp || !bp.id)
|
|
219
|
+
return { id, props: props };
|
|
220
|
+
const d = bp.draft ?? {};
|
|
221
|
+
return {
|
|
222
|
+
id,
|
|
223
|
+
props: {
|
|
224
|
+
...props,
|
|
225
|
+
name: bp.name,
|
|
226
|
+
instructions: d.instructions ?? undefined,
|
|
227
|
+
model: d.model ?? undefined,
|
|
228
|
+
enabledHostedTools: d.enabled_hosted_tools ?? [],
|
|
229
|
+
autoMemory: d.auto_memory ?? undefined,
|
|
230
|
+
variables: d.variables ?? [],
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
class IcBlueprint extends pulumi.dynamic.Resource {
|
|
236
|
+
constructor(name, args, opts) {
|
|
237
|
+
super(blueprintProvider, name, {
|
|
238
|
+
instructions: undefined,
|
|
239
|
+
model: undefined,
|
|
240
|
+
enabledHostedTools: undefined,
|
|
241
|
+
autoMemory: undefined,
|
|
242
|
+
variables: undefined,
|
|
243
|
+
publishNote: undefined,
|
|
244
|
+
rolloutPercent: undefined,
|
|
245
|
+
blueprintId: undefined,
|
|
246
|
+
activeVersion: undefined,
|
|
247
|
+
contentSig: undefined,
|
|
248
|
+
...args,
|
|
249
|
+
}, { ...opts, additionalSecretOutputs: ["token"] });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.IcBlueprint = IcBlueprint;
|
|
253
|
+
function mcpBody(i) {
|
|
254
|
+
const auth = { kind: i.authKind };
|
|
255
|
+
if (i.authProvider)
|
|
256
|
+
auth.provider = i.authProvider;
|
|
257
|
+
if (i.secret)
|
|
258
|
+
auth.secret = i.secret;
|
|
259
|
+
return { url: i.url, auth };
|
|
260
|
+
}
|
|
261
|
+
async function mcpPut(i) {
|
|
262
|
+
const res = await icRequest(i.baseUrl, i.token, "PUT", `/v1/tenant/mcp/${encodeURIComponent(i.serverName)}`, mcpBody(i));
|
|
263
|
+
return {
|
|
264
|
+
...i,
|
|
265
|
+
toolsDiscovered: res.tools_discovered ?? 0,
|
|
266
|
+
discoveryError: res.discovery_error ?? null,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const mcpProvider = {
|
|
270
|
+
async create(i) {
|
|
271
|
+
return { id: i.serverName, outs: await mcpPut(i) };
|
|
272
|
+
},
|
|
273
|
+
async update(_id, _olds, news) {
|
|
274
|
+
return { outs: await mcpPut(news) };
|
|
275
|
+
},
|
|
276
|
+
async delete(id, props) {
|
|
277
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/tenant/mcp/${encodeURIComponent(id)}`);
|
|
278
|
+
},
|
|
279
|
+
async diff(_id, olds, news) {
|
|
280
|
+
const replaces = olds.serverName !== news.serverName ? ["serverName"] : [];
|
|
281
|
+
const changed = replaces.length > 0 ||
|
|
282
|
+
["url", "authKind", "authProvider", "secret", "token", "baseUrl"].some((k) => olds[k] !== news[k]);
|
|
283
|
+
return { changes: changed, replaces, deleteBeforeReplace: true };
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
class IcMcpServer extends pulumi.dynamic.Resource {
|
|
287
|
+
constructor(name, args, opts) {
|
|
288
|
+
super(mcpProvider, name, {
|
|
289
|
+
authKind: "static",
|
|
290
|
+
authProvider: undefined,
|
|
291
|
+
secret: undefined,
|
|
292
|
+
toolsDiscovered: undefined,
|
|
293
|
+
discoveryError: undefined,
|
|
294
|
+
...args,
|
|
295
|
+
}, { ...opts, additionalSecretOutputs: ["token", "secret"] });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
exports.IcMcpServer = IcMcpServer;
|
|
299
|
+
async function telegramPut(i) {
|
|
300
|
+
const res = await icRequest(i.baseUrl, i.token, "PUT", "/v1/tenant/telegram", {
|
|
301
|
+
bot_token: i.botToken,
|
|
302
|
+
});
|
|
303
|
+
return { ...i, botUsername: res.bot_username, botId: res.bot_id, webhookUrl: res.webhook_url };
|
|
304
|
+
}
|
|
305
|
+
const telegramProvider = {
|
|
306
|
+
async create(i) {
|
|
307
|
+
return { id: "telegram", outs: await telegramPut(i) };
|
|
308
|
+
},
|
|
309
|
+
async update(_id, _olds, news) {
|
|
310
|
+
return { outs: await telegramPut(news) };
|
|
311
|
+
},
|
|
312
|
+
async delete(_id, props) {
|
|
313
|
+
await icRequest(props.baseUrl, props.token, "DELETE", "/v1/tenant/telegram");
|
|
314
|
+
},
|
|
315
|
+
async diff(_id, olds, news) {
|
|
316
|
+
return {
|
|
317
|
+
changes: ["botToken", "token", "baseUrl"].some((k) => olds[k] !== news[k]),
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
class IcTelegramBot extends pulumi.dynamic.Resource {
|
|
322
|
+
constructor(name, args, opts) {
|
|
323
|
+
super(telegramProvider, name, { botUsername: undefined, botId: undefined, webhookUrl: undefined, ...args }, { ...opts, additionalSecretOutputs: ["token", "botToken"] });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
exports.IcTelegramBot = IcTelegramBot;
|
|
327
|
+
async function whatsappPut(i) {
|
|
328
|
+
const res = await icRequest(i.baseUrl, i.token, "PUT", "/v1/tenant/whatsapp", {
|
|
329
|
+
phone_number_id: i.phoneNumberId,
|
|
330
|
+
access_token: i.accessToken,
|
|
331
|
+
app_secret: i.appSecret,
|
|
332
|
+
verify_token: i.verifyToken,
|
|
333
|
+
business_id: i.businessId,
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
...i,
|
|
337
|
+
// IC generates verify_token if we didn't supply one — capture it so it's
|
|
338
|
+
// stable in state (and so the operator can paste it into Meta).
|
|
339
|
+
verifyToken: res.verify_token ?? i.verifyToken,
|
|
340
|
+
displayPhoneNumber: res.display_phone_number,
|
|
341
|
+
webhookUrl: res.webhook_url,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const whatsappProvider = {
|
|
345
|
+
async create(i) {
|
|
346
|
+
return { id: "whatsapp", outs: await whatsappPut(i) };
|
|
347
|
+
},
|
|
348
|
+
async update(_id, _olds, news) {
|
|
349
|
+
return { outs: await whatsappPut(news) };
|
|
350
|
+
},
|
|
351
|
+
async delete(_id, props) {
|
|
352
|
+
await icRequest(props.baseUrl, props.token, "DELETE", "/v1/tenant/whatsapp");
|
|
353
|
+
},
|
|
354
|
+
async diff(_id, olds, news) {
|
|
355
|
+
return {
|
|
356
|
+
changes: ["phoneNumberId", "accessToken", "appSecret", "businessId", "token", "baseUrl"].some((k) => olds[k] !== news[k]),
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
class IcWhatsAppConfig extends pulumi.dynamic.Resource {
|
|
361
|
+
constructor(name, args, opts) {
|
|
362
|
+
super(whatsappProvider, name, {
|
|
363
|
+
appSecret: undefined,
|
|
364
|
+
verifyToken: undefined,
|
|
365
|
+
businessId: undefined,
|
|
366
|
+
displayPhoneNumber: undefined,
|
|
367
|
+
webhookUrl: undefined,
|
|
368
|
+
...args,
|
|
369
|
+
}, { ...opts, additionalSecretOutputs: ["token", "accessToken", "appSecret", "verifyToken"] });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
exports.IcWhatsAppConfig = IcWhatsAppConfig;
|
|
373
|
+
const webhookProvider = {
|
|
374
|
+
async create(i) {
|
|
375
|
+
const res = await icRequest(i.baseUrl, i.token, "POST", "/v1/tenant/webhooks", {
|
|
376
|
+
url: i.url,
|
|
377
|
+
events: i.events,
|
|
378
|
+
active: true,
|
|
379
|
+
});
|
|
380
|
+
return { id: res.id, outs: { ...i, secret: res.secret } };
|
|
381
|
+
},
|
|
382
|
+
async update(id, _olds, news) {
|
|
383
|
+
await icRequest(news.baseUrl, news.token, "PATCH", `/v1/tenant/webhooks/${id}`, {
|
|
384
|
+
events: news.events,
|
|
385
|
+
});
|
|
386
|
+
return { outs: { ...news } };
|
|
387
|
+
},
|
|
388
|
+
async delete(id, props) {
|
|
389
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/tenant/webhooks/${id}`);
|
|
390
|
+
},
|
|
391
|
+
async diff(_id, olds, news) {
|
|
392
|
+
const replaces = olds.url !== news.url ? ["url"] : [];
|
|
393
|
+
const eventsChanged = JSON.stringify([...(olds.events ?? [])].sort()) !==
|
|
394
|
+
JSON.stringify([...(news.events ?? [])].sort());
|
|
395
|
+
return { changes: replaces.length > 0 || eventsChanged, replaces };
|
|
396
|
+
},
|
|
397
|
+
// Enables `pulumi import` of a webhook a script already created (id is the
|
|
398
|
+
// import id). The signing secret is intentionally not recoverable.
|
|
399
|
+
async read(id, props) {
|
|
400
|
+
const list = await icRequest(props.baseUrl, props.token, "GET", "/v1/tenant/webhooks");
|
|
401
|
+
const hit = (list.data ?? []).find((w) => w.id === id);
|
|
402
|
+
if (!hit)
|
|
403
|
+
return { id, props: props };
|
|
404
|
+
return { id, props: { ...props, url: hit.url, events: hit.events } };
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
class IcWebhook extends pulumi.dynamic.Resource {
|
|
408
|
+
constructor(name, args, opts) {
|
|
409
|
+
super(webhookProvider, name, { secret: undefined, ...args }, {
|
|
410
|
+
...opts,
|
|
411
|
+
additionalSecretOutputs: ["token", "secret"],
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
exports.IcWebhook = IcWebhook;
|
|
416
|
+
async function modelKeyPut(i) {
|
|
417
|
+
await icRequest(i.baseUrl, i.token, "PUT", `/v1/tenant/model_keys/${encodeURIComponent(i.provider)}`, { api_key: i.apiKey, base_url: i.providerBaseUrl ?? null });
|
|
418
|
+
return { ...i };
|
|
419
|
+
}
|
|
420
|
+
const modelKeyProvider = {
|
|
421
|
+
async create(i) {
|
|
422
|
+
return { id: i.provider, outs: await modelKeyPut(i) };
|
|
423
|
+
},
|
|
424
|
+
async update(_id, _olds, news) {
|
|
425
|
+
return { outs: await modelKeyPut(news) };
|
|
426
|
+
},
|
|
427
|
+
async delete(id, props) {
|
|
428
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/tenant/model_keys/${encodeURIComponent(id)}`);
|
|
429
|
+
},
|
|
430
|
+
async diff(_id, olds, news) {
|
|
431
|
+
const replaces = olds.provider !== news.provider ? ["provider"] : [];
|
|
432
|
+
const changed = replaces.length > 0 ||
|
|
433
|
+
["apiKey", "providerBaseUrl", "token", "baseUrl"].some((k) => olds[k] !== news[k]);
|
|
434
|
+
return { changes: changed, replaces, deleteBeforeReplace: true };
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
class IcModelKey extends pulumi.dynamic.Resource {
|
|
438
|
+
constructor(name, args, opts) {
|
|
439
|
+
super(modelKeyProvider, name, { providerBaseUrl: undefined, ...args }, { ...opts, additionalSecretOutputs: ["token", "apiKey"] });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
exports.IcModelKey = IcModelKey;
|
|
443
|
+
async function secretPut(i) {
|
|
444
|
+
await icRequest(i.baseUrl, i.token, "PUT", `/v1/apps/${encodeURIComponent(i.appId)}/env`, {
|
|
445
|
+
values: { [i.name]: i.value },
|
|
446
|
+
});
|
|
447
|
+
return { ...i };
|
|
448
|
+
}
|
|
449
|
+
const secretProvider = {
|
|
450
|
+
async create(i) {
|
|
451
|
+
return { id: `${i.appId}:${i.name}`, outs: await secretPut(i) };
|
|
452
|
+
},
|
|
453
|
+
async update(_id, _olds, news) {
|
|
454
|
+
return { outs: await secretPut(news) };
|
|
455
|
+
},
|
|
456
|
+
async delete(_id, props) {
|
|
457
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/apps/${encodeURIComponent(props.appId)}/env/${encodeURIComponent(props.name)}`);
|
|
458
|
+
},
|
|
459
|
+
async diff(_id, olds, news) {
|
|
460
|
+
const replaces = [];
|
|
461
|
+
if (olds.appId !== news.appId)
|
|
462
|
+
replaces.push("appId");
|
|
463
|
+
if (olds.name !== news.name)
|
|
464
|
+
replaces.push("name");
|
|
465
|
+
const changed = replaces.length > 0 ||
|
|
466
|
+
["value", "token", "baseUrl"].some((k) => olds[k] !== news[k]);
|
|
467
|
+
return { changes: changed, replaces, deleteBeforeReplace: true };
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
class IcSecret extends pulumi.dynamic.Resource {
|
|
471
|
+
constructor(name, args, opts) {
|
|
472
|
+
super(secretProvider, name, { ...args }, {
|
|
473
|
+
...opts,
|
|
474
|
+
additionalSecretOutputs: ["token", "value"],
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
exports.IcSecret = IcSecret;
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingram-tech/pulumi-ingram-cloud",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pulumi dynamic resources for the Ingram Cloud /v1 API — blueprints, MCP servers, channels, webhooks, model keys.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ingram-technologies/cloud.ingram.tech.git",
|
|
9
|
+
"directory": "pulumi"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ingram-technologies/cloud.ingram.tech/tree/main/pulumi#readme",
|
|
12
|
+
"keywords": ["pulumi", "ingram-cloud", "ingram"],
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@pulumi/pulumi": "^3.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@pulumi/pulumi": "^3.0.0",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|