@myapihq/sdk 2.4.0 → 2.4.2

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/src/auth.ts DELETED
@@ -1,161 +0,0 @@
1
- import { request } from './client';
2
- import { AUTH_BASE as BASE_URL } from './config';
3
- import type { Exposes } from './exposes';
4
-
5
- // The end-user auth product (my-auth-api): a managed multi-tenant OIDC
6
- // Identity Provider for the END USERS of apps built on MyAPI. Distinct from
7
- // operator/account auth (hq/account/*). Each org gets one auth tenant; OIDC
8
- // clients (apps) register against it. End users sign in with managed Google,
9
- // email/password, or magic links (per the tenant's `connections`). Tokens are
10
- // RS256, verified via the tenant's JWKS.
11
- //
12
- // The browser/app-facing OIDC + sign-in flows — discovery, jwks, authorize,
13
- // token, userinfo, password/magic/google login, verify-email, password reset,
14
- // the hosted login page — are machine/end-user surfaces consumed by apps, the
15
- // JS SDK, and browsers, NOT management calls; they are not CLI/SDK verbs. This
16
- // module covers only the org-scoped MANAGEMENT surface: tenant, clients, MAU
17
- // usage, and the custom auth domain.
18
- export const EXPOSES: Exposes = [
19
- 'POST /auth/orgs/{org_id}/tenant',
20
- 'GET /auth/orgs/{org_id}/tenant',
21
- 'POST /auth/orgs/{org_id}/clients',
22
- 'GET /auth/orgs/{org_id}/clients',
23
- 'DELETE /auth/orgs/{org_id}/clients/{client_id}',
24
- 'POST /auth/orgs/{org_id}/clients/{client_id}/rotate',
25
- 'GET /auth/orgs/{org_id}/usage',
26
- 'POST /auth/orgs/{org_id}/domain',
27
- 'GET /auth/orgs/{org_id}/domain',
28
- 'POST /auth/orgs/{org_id}/domain/verify',
29
- 'DELETE /auth/orgs/{org_id}/domain',
30
- ];
31
-
32
- // Sign-in methods a tenant offers on its hosted login page.
33
- export type AuthConnection = 'google' | 'password' | 'magic';
34
-
35
- export interface Tenant {
36
- tenant_id: string;
37
- issuer: string; // OIDC issuer: <base>/<tenant_id>
38
- login_url: string; // hosted authorize endpoint
39
- connections?: AuthConnection[];
40
- }
41
-
42
- export interface CreateTenantInput {
43
- // Sign-in methods to enable. Defaults to ['google'] when omitted.
44
- connections?: AuthConnection[];
45
- // Opaque JSON for the hosted login page (branding).
46
- theme?: unknown;
47
- }
48
-
49
- export interface AuthClient {
50
- client_id: string;
51
- name?: string;
52
- type: 'spa' | 'web';
53
- redirect_uris: string[];
54
- issuer?: string;
55
- // Returned exactly ONCE, on create, for confidential ('web') clients.
56
- // SPA clients are public and have no secret.
57
- client_secret?: string;
58
- created_at?: string;
59
- }
60
-
61
- export interface ListClientsResponse {
62
- clients: AuthClient[];
63
- }
64
-
65
- export interface CreateClientInput {
66
- name: string;
67
- type: 'spa' | 'web';
68
- redirect_uris: string[];
69
- }
70
-
71
- // Monthly-active-user metering for the tenant (auth is billed per MAU).
72
- export interface AuthUsage {
73
- period: string; // 'YYYY-MM'
74
- active_users: number;
75
- price_cents_each: number;
76
- as_of?: string; // RFC3339 freshness marker — MAU is aggregated, not real-time
77
- }
78
-
79
- // A custom auth domain (e.g. auth.acme.com) for the tenant's hosted login +
80
- // issuer. Lifecycle: awaiting_verification (publish the TXT challenge, then
81
- // `verify`) → pending (publish the A record, TLS provisions) → active.
82
- export interface AuthDomain {
83
- domain: string;
84
- status: 'awaiting_verification' | 'pending' | 'active';
85
- // Present while `awaiting_verification` — the ownership-challenge TXT record
86
- // to publish, then call verifyDomain().
87
- verification?: { type: string; name: string; value: string };
88
- // Present once `pending`/`active` — the A record to publish (withheld until
89
- // ownership is verified).
90
- dns?: { type: string; name: string; value: string };
91
- next?: string;
92
- issuer?: string; // present once active
93
- login_url?: string; // present once active
94
- }
95
-
96
- // Create (or update) the org's auth tenant. Idempotent. `connections` selects
97
- // the sign-in methods (subset of google/password/magic; defaults to google);
98
- // `theme` is opaque JSON for the hosted login page.
99
- export async function createTenant(apiKey: string, orgId: string, input: CreateTenantInput = {}): Promise<Tenant> {
100
- const body: Record<string, unknown> = {};
101
- if (input.connections !== undefined) body.connections = input.connections;
102
- if (input.theme !== undefined) body.theme = input.theme;
103
- return request('POST', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/tenant`, apiKey, body);
104
- }
105
-
106
- // Fetch the org's auth tenant. Rejects with TENANT_NOT_FOUND if not created.
107
- export async function getTenant(apiKey: string, orgId: string): Promise<Tenant> {
108
- return request('GET', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/tenant`, apiKey);
109
- }
110
-
111
- // Register an OIDC client (an app) under the org's tenant. Auto-provisions the
112
- // tenant if absent. For type 'web' the response carries `client_secret` once.
113
- export async function createClient(apiKey: string, orgId: string, input: CreateClientInput): Promise<AuthClient> {
114
- return request('POST', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/clients`, apiKey, input);
115
- }
116
-
117
- export async function listClients(apiKey: string, orgId: string): Promise<ListClientsResponse> {
118
- return request('GET', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/clients`, apiKey);
119
- }
120
-
121
- // Delete (revoke) an OIDC client. Irreversible — the client_id stops
122
- // authenticating immediately.
123
- export async function deleteClient(apiKey: string, orgId: string, clientId: string): Promise<{ client_id: string; deleted: boolean }> {
124
- return request('DELETE', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/clients/${encodeURIComponent(clientId)}`, apiKey);
125
- }
126
-
127
- // Rotate a confidential ('web') client's secret. Returns the NEW secret exactly
128
- // once; the previous secret stops working immediately. (SPA clients have no
129
- // secret — rotating one is rejected by the backend.)
130
- export async function rotateClient(apiKey: string, orgId: string, clientId: string): Promise<AuthClient> {
131
- return request('POST', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/clients/${encodeURIComponent(clientId)}/rotate`, apiKey);
132
- }
133
-
134
- // Monthly-active-user usage for the current period (auth is billed per MAU).
135
- export async function getUsage(apiKey: string, orgId: string): Promise<AuthUsage> {
136
- return request('GET', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/usage`, apiKey);
137
- }
138
-
139
- // Set a custom auth domain (e.g. auth.acme.com). Returns the DNS record to
140
- // create; TLS provisions automatically and the domain becomes the issuer once
141
- // active. One custom domain per org.
142
- export async function registerDomain(apiKey: string, orgId: string, domain: string): Promise<AuthDomain> {
143
- return request('POST', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/domain`, apiKey, { domain });
144
- }
145
-
146
- // Fetch the org's custom auth domain. Rejects with NO_DOMAIN if none set.
147
- export async function getDomain(apiKey: string, orgId: string): Promise<AuthDomain> {
148
- return request('GET', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/domain`, apiKey);
149
- }
150
-
151
- // Verify ownership of the org's custom auth domain by checking the published
152
- // TXT challenge. On success the domain advances awaiting_verification → pending
153
- // (and the response carries the A record). Rejects with 422 DOMAIN_NOT_VERIFIED
154
- // if the TXT record isn't found yet. Idempotent once already pending/active.
155
- export async function verifyDomain(apiKey: string, orgId: string): Promise<AuthDomain> {
156
- return request('POST', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/domain/verify`, apiKey);
157
- }
158
-
159
- export async function deleteDomain(apiKey: string, orgId: string, domain: string): Promise<{ domain: string; status: string }> {
160
- return request('DELETE', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/domain`, apiKey, { domain });
161
- }
package/src/client.ts DELETED
@@ -1,121 +0,0 @@
1
- import { ApiResponse } from './types';
2
-
3
- export class MyApiError extends Error {
4
- // `body` is the full error object as returned by the backend — preserved so
5
- // callers can read service-specific fields the SDK doesn't otherwise model
6
- // (e.g. CF_API_ERROR carries `cf_message` / `cf_status`).
7
- constructor(public code: string, public status: number, public detail?: string, public body?: Record<string, unknown>) {
8
- // Surface the detail in the message so console.error / err.message
9
- // shows useful info without callers having to look at err.detail.
10
- super(detail ? `${code} (${status}): ${detail}` : code);
11
- this.name = 'MyApiError';
12
- }
13
- }
14
-
15
- async function requestFull<T>(
16
- method: string,
17
- url: string,
18
- apiKey?: string,
19
- body?: unknown,
20
- extraHeaders?: Record<string, string>,
21
- ): Promise<ApiResponse<T>> {
22
- const headers: Record<string, string> = {};
23
-
24
- if (apiKey) {
25
- headers['Authorization'] = `Bearer ${apiKey}`;
26
- }
27
-
28
- if (body !== undefined) {
29
- headers['Content-Type'] = 'application/json';
30
- }
31
-
32
- // Caller-supplied headers (e.g. CAS via If-Match) win over the defaults
33
- // above. Keep this near the top so route logic later doesn't accidentally
34
- // overwrite something the caller passed in.
35
- if (extraHeaders) {
36
- for (const [k, v] of Object.entries(extraHeaders)) headers[k] = v;
37
- }
38
-
39
- const options: RequestInit = {
40
- method,
41
- headers,
42
- };
43
-
44
- if (body !== undefined) {
45
- options.body = JSON.stringify(body);
46
- }
47
-
48
- const response = await fetch(url, options);
49
-
50
- if (response.status === 204) {
51
- return { success: true, data: null, error: null, meta: {} as ApiResponse<T>['meta'] };
52
- }
53
-
54
- // Read the body as text first so we can include a snippet in errors when
55
- // it isn't valid JSON (HTML error pages from edge proxies, plaintext
56
- // 5xx, etc.). One read; we re-parse manually below.
57
- const raw = await response.text();
58
-
59
- let result: any;
60
- try {
61
- result = raw ? JSON.parse(raw) : {};
62
- } catch {
63
- const snippet = raw.slice(0, 200).replace(/\s+/g, ' ').trim();
64
- throw new MyApiError(
65
- 'invalid_json_response',
66
- response.status,
67
- `${method} ${url} returned non-JSON (status ${response.status}): ${snippet}`,
68
- );
69
- }
70
-
71
- if (!response.ok) {
72
- const err = result?.error;
73
- const code = typeof err === 'object' ? (err?.code || 'unknown_error') : (err || 'unknown_error');
74
- const detail = typeof err === 'object' ? (err?.message || undefined) : undefined;
75
- const errBody = typeof err === 'object' ? err : undefined;
76
- throw new MyApiError(code, response.status, detail, errBody);
77
- }
78
-
79
- const apiResponse = result as ApiResponse<T>;
80
- if (!apiResponse.success) {
81
- const err = apiResponse.error as any;
82
- const code = typeof err === 'object' ? (err?.code || 'unknown_error') : (err || 'unknown_error');
83
- const detail = typeof err === 'object' ? (err?.message || undefined) : undefined;
84
- const errBody = typeof err === 'object' ? err : undefined;
85
- throw new MyApiError(code, response.status, detail, errBody);
86
- }
87
-
88
- return apiResponse;
89
- }
90
-
91
- // Returns the unwrapped `data` payload — the common case.
92
- export async function request<T>(
93
- method: string,
94
- url: string,
95
- apiKey?: string,
96
- body?: unknown,
97
- extraHeaders?: Record<string, string>,
98
- ): Promise<T> {
99
- return (await requestFull<T>(method, url, apiKey, body, extraHeaders)).data as T;
100
- }
101
-
102
- export interface Page<T> {
103
- data: T[];
104
- next_cursor?: string;
105
- has_more?: boolean;
106
- }
107
-
108
- // For keyset-paginated endpoints (backend `envelope.WrapPage`): `data` is a
109
- // bare array and the cursor/has_more live under `meta`. `request` would drop
110
- // the cursor, so list endpoints with pagination must use this.
111
- export async function requestPage<T>(
112
- method: string,
113
- url: string,
114
- apiKey?: string,
115
- body?: unknown,
116
- extraHeaders?: Record<string, string>,
117
- ): Promise<Page<T>> {
118
- const r = await requestFull<T[]>(method, url, apiKey, body, extraHeaders);
119
- const meta = (r.meta ?? {}) as { next_cursor?: string; has_more?: boolean };
120
- return { data: (r.data ?? []) as T[], next_cursor: meta.next_cursor, has_more: meta.has_more };
121
- }
package/src/company.ts DELETED
@@ -1,48 +0,0 @@
1
- // Scaffolded from schema by scripts/scaffold-sdk.js — DO NOT remove the
2
- // EXPOSES array (the coverage tool verifies it against the live schema).
3
- // Types and function names ARE editable; re-running the scaffolder against
4
- // a service whose config has been changed will diff vs. existing file
5
- // (use --dry-run to inspect).
6
-
7
- import { request } from './client';
8
- import { COMPANY_BASE as BASE_URL } from './config';
9
- import type { Exposes } from './exposes';
10
-
11
- import type { SearchFilter } from './audience';
12
- import type { Person } from './people';
13
-
14
- export const EXPOSES: Exposes = [
15
- 'POST /company/orgs/{org_id}/search',
16
- 'GET /company/orgs/{org_id}/{company_id}',
17
- ];
18
-
19
- import type { Location, PersonCompany } from './people';
20
-
21
- // Company is the same shape as PersonCompany (a person's embedded company)
22
- // with an optional embedded people array when include_people=N is passed.
23
- export interface Company extends PersonCompany {
24
- people?: Person[]; // present when include_people=N is passed
25
- }
26
-
27
- export interface CompanySearchResult {
28
- companies: Company[];
29
- total: number;
30
- limit: number;
31
- offset: number;
32
- has_more: boolean;
33
- }
34
-
35
- export interface CompanySearchOptions extends SearchFilter {
36
- include_people?: number; // 0..10; embed up to N people per company
37
- }
38
-
39
- export async function searchCompanies(apiKey: string, orgId: string, options: CompanySearchOptions): Promise<CompanySearchResult> {
40
- return request('POST', `${BASE_URL}/company/orgs/${encodeURIComponent(orgId)}/search`, apiKey, options);
41
- }
42
-
43
- export async function getCompany(apiKey: string, orgId: string, companyId: string, include_people?: number): Promise<Company> {
44
- const _url = include_people != null
45
- ? `${BASE_URL}/company/orgs/${encodeURIComponent(orgId)}/${encodeURIComponent(companyId)}?include_people=${encodeURIComponent(String(include_people))}`
46
- : `${BASE_URL}/company/orgs/${encodeURIComponent(orgId)}/${encodeURIComponent(companyId)}`;
47
- return request('GET', _url, apiKey);
48
- }
package/src/config.ts DELETED
@@ -1,39 +0,0 @@
1
- // Service base URLs. Today's reality: all primitives are served from the
2
- // single `api.myapihq.com` gateway; per-service brand hosts
3
- // (e.g. api.myllmapi.com) are aspirational and only some are wired in DNS
4
- // + routed correctly. We default to the working hosts where they exist,
5
- // and to the gateway where they don't (LLM, DATABASE, CRM, PEOPLE,
6
- // COMPANY, AUDIENCE). The MYAPI_*_URL env vars stay as overrides for
7
- // local dev — `source scripts/local-env.sh` still points everything at
8
- // localhost:8080 regardless of the prod default.
9
-
10
- const GATEWAY = 'https://api.myapihq.com';
11
-
12
- export const HQ_BASE = process.env.MYAPI_HQ_URL ?? process.env.MYAPI_API_BASE ?? GATEWAY;
13
- export const DOMAIN_BASE = process.env.MYAPI_DOMAIN_URL ?? 'https://api.mydomainapi.com';
14
- export const FUNNEL_BASE = process.env.MYAPI_FUNNEL_URL ?? 'https://api.myfunnelapi.com';
15
- // New slots — routed via the gateway until brand hosts are DNS-wired
16
- // (mirrors the LLM/DATABASE/CRM pattern below).
17
- export const FUNCTION_BASE= process.env.MYAPI_FUNCTION_URL?? GATEWAY;
18
- export const PAYMENTS_BASE= process.env.MYAPI_PAYMENTS_URL?? GATEWAY;
19
- export const CONTAINER_BASE=process.env.MYAPI_CONTAINER_URL?? GATEWAY;
20
- export const GIT_BASE = process.env.MYAPI_GIT_URL ?? GATEWAY;
21
- export const QUEUE_BASE = process.env.MYAPI_QUEUE_URL ?? GATEWAY;
22
- export const TASK_BASE = process.env.MYAPI_TASK_URL ?? GATEWAY;
23
- export const IMAGE_BASE = process.env.MYAPI_IMAGE_URL ?? 'https://api.myimageapi.com';
24
- export const WEBHOOK_BASE = process.env.MYAPI_WEBHOOK_URL ?? 'https://api.mywebhookapi.com';
25
- export const WORKFLOW_BASE= process.env.MYAPI_WORKFLOW_URL?? 'https://api.myworkflowapi.com';
26
- export const EMAIL_BASE = process.env.MYAPI_EMAIL_URL ?? 'https://api.myemailapi.com';
27
- export const STORAGE_BASE = process.env.MYAPI_STORAGE_URL ?? 'https://api.mystorageapi.com';
28
- export const URL_BASE = process.env.MYAPI_URL_URL ?? 'https://api.myurlto.com';
29
- export const PIXEL_BASE = process.env.MYAPI_PIXEL_URL ?? 'https://api.mypixelapi.com';
30
- // Per-service hosts below either don't resolve in DNS or 404 the routes;
31
- // served via the gateway until backend wires the brand routing. Flip back
32
- // to per-service hosts when DNS is sound and routes verified there.
33
- export const PEOPLE_BASE = process.env.MYAPI_PEOPLE_URL ?? GATEWAY;
34
- export const COMPANY_BASE = process.env.MYAPI_COMPANY_URL ?? GATEWAY;
35
- export const AUDIENCE_BASE= process.env.MYAPI_AUDIENCE_URL?? GATEWAY;
36
- export const LLM_BASE = process.env.MYAPI_LLM_URL ?? GATEWAY;
37
- export const DATABASE_BASE= process.env.MYAPI_DATABASE_URL?? GATEWAY;
38
- export const CRM_BASE = process.env.MYAPI_CRM_URL ?? GATEWAY;
39
- export const AUTH_BASE = process.env.MYAPI_AUTH_URL ?? GATEWAY;
package/src/container.ts DELETED
@@ -1,187 +0,0 @@
1
- import { request, MyApiError } from './client';
2
- import { ApiResponse } from './types';
3
- import { CONTAINER_BASE as BASE_URL } from './config';
4
- import type { Exposes } from './exposes';
5
-
6
- // Backend: my-container-api per myapi-hq/internal/routes/container/. Phase 1
7
- // is metadata + scoped API key issuance; the Cloud Run build/deploy pipeline
8
- // is Phase 2 — `deployContainer` returns RUNTIME_UNAVAILABLE until it lands.
9
- export const EXPOSES: Exposes = [
10
- 'POST /container/orgs/{org_id}/containers',
11
- 'GET /container/orgs/{org_id}/containers',
12
- 'GET /container/orgs/{org_id}/containers/{id}',
13
- 'DELETE /container/orgs/{org_id}/containers/{id}',
14
- 'POST /container/orgs/{org_id}/containers/{id}/deploy',
15
- 'GET /container/orgs/{org_id}/containers/{id}/logs',
16
- 'POST /container/orgs/{org_id}/containers/{id}/domain',
17
- 'DELETE /container/orgs/{org_id}/containers/{id}/domain',
18
- ];
19
-
20
- // service = HTTP server, worker = always-on background process, job = runs
21
- // to completion (the only type that accepts a cron_schedule).
22
- export type ContainerType = 'service' | 'worker' | 'job';
23
-
24
- // Mirrors the backend's `Container` struct in crud.go. `url` is empty until
25
- // the container is deployed.
26
- export interface Container {
27
- id: string;
28
- org_id: string;
29
- name: string;
30
- type: ContainerType;
31
- cron_schedule?: string;
32
- env: Record<string, unknown>;
33
- cpu: string;
34
- memory: string;
35
- min_instances: number;
36
- max_instances: number;
37
- port: number;
38
- url?: string;
39
- // Custom domain bound to this container, if any. Served via a Cloudflare
40
- // Origin Rule that rewrites Host + SNI to the Cloud Run hostname.
41
- custom_domain?: string;
42
- status: string;
43
- // Egress mode for the container's outbound traffic (backend returns it on
44
- // every row, container/crud.go). Static-IP vs. default depending on config.
45
- egress: string;
46
- created_at: string;
47
- updated_at: string;
48
- }
49
-
50
- export interface CreatePayload {
51
- name: string; // required; ^[a-z0-9][a-z0-9-]{0,49}$
52
- type?: ContainerType; // defaults to 'service' server-side
53
- cron_schedule?: string; // only valid when type is 'job'
54
- env?: Record<string, unknown>;
55
- cpu?: string; // defaults to '1'
56
- memory?: string; // defaults to '512Mi'
57
- min_instances?: number; // defaults to 0 ('worker' is forced to >= 1)
58
- max_instances?: number; // defaults to 3
59
- port?: number;
60
- }
61
-
62
- // POST response — the scoped API key is returned ONCE. It is delivered to
63
- // the running container as the MYAPI_KEY environment variable.
64
- export interface CreateResponse {
65
- container: Container;
66
- scoped_api_key: string;
67
- scoped_api_key_id: string;
68
- }
69
-
70
- // deployContainer response. The scoped key is ROTATED on every deploy — the
71
- // value here is fresh and the only time it is knowable.
72
- export interface DeployResponse {
73
- container_id: string;
74
- revision_id: string;
75
- url: string;
76
- status: string;
77
- scoped_api_key: string;
78
- }
79
-
80
- export async function createContainer(apiKey: string, orgId: string, payload: CreatePayload): Promise<CreateResponse> {
81
- return request('POST', `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers`, apiKey, payload);
82
- }
83
-
84
- export async function listContainers(apiKey: string, orgId: string): Promise<Container[]> {
85
- return request('GET', `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers`, apiKey);
86
- }
87
-
88
- export async function getContainer(apiKey: string, orgId: string, containerId: string): Promise<Container> {
89
- return request('GET', `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers/${encodeURIComponent(containerId)}`, apiKey);
90
- }
91
-
92
- export async function deleteContainer(apiKey: string, orgId: string, containerId: string): Promise<void> {
93
- return request('DELETE', `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers/${encodeURIComponent(containerId)}`, apiKey);
94
- }
95
-
96
- // deployContainer ships a revision from a pre-built image ref to Cloud Run.
97
- // Synchronous — returns status='active' with a rotated scoped key.
98
- export async function deployContainer(apiKey: string, orgId: string, containerId: string, image: string): Promise<DeployResponse> {
99
- return request('POST', `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers/${encodeURIComponent(containerId)}/deploy`, apiKey, { image });
100
- }
101
-
102
- // BuildDeployResponse — the async source-build path. The backend records a
103
- // `building` revision, kicks off Cloud Build on a background goroutine, and
104
- // returns immediately. The scoped key is minted later (once the image
105
- // exists), so it is NOT in this response. Poll getContainer until
106
- // status='active' (or 'build_error'/'deploy_error').
107
- export interface BuildDeployResponse {
108
- container_id: string;
109
- revision_id: string;
110
- status: string; // 'building'
111
- message?: string;
112
- }
113
-
114
- // deployContainerSource uploads a build-context tarball via multipart `source`
115
- // (backend deploy.go builds it via Cloud Build → Artifact Registry, then
116
- // deploys). Asynchronous: returns status='building' to poll. The tarball is
117
- // capped at 100MB server-side. Mirrors the multipart upload in storage.ts.
118
- export async function deployContainerSource(
119
- apiKey: string,
120
- orgId: string,
121
- containerId: string,
122
- tarball: Blob | Buffer,
123
- filename = 'source.tar.gz',
124
- ): Promise<BuildDeployResponse> {
125
- const formData = new FormData();
126
- formData.append('source', new Blob([tarball as any], { type: 'application/gzip' }), filename);
127
-
128
- const response = await fetch(
129
- `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers/${encodeURIComponent(containerId)}/deploy`,
130
- { method: 'POST', headers: { Authorization: `Bearer ${apiKey}` }, body: formData as any },
131
- );
132
-
133
- let result: any;
134
- try {
135
- result = await response.json();
136
- } catch {
137
- throw new MyApiError('invalid_json_response', response.status);
138
- }
139
- if (!response.ok || !result?.success) {
140
- const err = result?.error;
141
- const code = typeof err === 'object' ? (err?.code || 'unknown_error') : (err || 'unknown_error');
142
- const detail = typeof err === 'object' ? err?.message : undefined;
143
- throw new MyApiError(code, response.status, detail, typeof err === 'object' ? err : undefined);
144
- }
145
- return (result as ApiResponse<BuildDeployResponse>).data as BuildDeployResponse;
146
- }
147
-
148
- // One runtime log line from the container's Cloud Run resource.
149
- export interface LogEntry {
150
- timestamp: string;
151
- severity: string;
152
- text: string;
153
- }
154
-
155
- // getContainerLogs returns recent runtime log entries, newest first.
156
- // `tail` caps the count (default 100, max 1000 server-side). An
157
- // undeployed container returns an empty array.
158
- export async function getContainerLogs(apiKey: string, orgId: string, containerId: string, tail?: number): Promise<LogEntry[]> {
159
- const query = tail ? `?tail=${encodeURIComponent(String(tail))}` : '';
160
- return request('GET', `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers/${encodeURIComponent(containerId)}/logs${query}`, apiKey);
161
- }
162
-
163
- // bindDomain response. `origin` is the Cloud Run hostname the custom domain
164
- // is fronted onto via a Cloudflare Origin Rule (Host + SNI rewrite).
165
- export interface DomainBinding {
166
- container_id: string;
167
- custom_domain: string;
168
- origin: string;
169
- status: string;
170
- }
171
-
172
- // bindDomain points a custom domain at a deployed container. The parent
173
- // domain must be MyAPI-managed. Errors: 422 (container not deployed, or no
174
- // MyAPI-managed parent domain), 409 (container already has a domain, or the
175
- // hostname is taken).
176
- function domainUrl(orgId: string, containerId: string): string {
177
- return `${BASE_URL}/container/orgs/${encodeURIComponent(orgId)}/containers/${encodeURIComponent(containerId)}/domain`;
178
- }
179
-
180
- export async function bindDomain(apiKey: string, orgId: string, containerId: string, domain: string): Promise<DomainBinding> {
181
- return request('POST', domainUrl(orgId, containerId), apiKey, { domain });
182
- }
183
-
184
- // unbindDomain removes the custom domain from a container.
185
- export async function unbindDomain(apiKey: string, orgId: string, containerId: string): Promise<void> {
186
- return request('DELETE', domainUrl(orgId, containerId), apiKey);
187
- }