@outposted/node 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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/package.json +58 -0
- package/src/client.ts +91 -0
- package/src/errors.ts +214 -0
- package/src/index.ts +37 -0
- package/src/resources/api-keys.ts +157 -0
- package/src/resources/brands.ts +127 -0
- package/src/resources/connections.ts +115 -0
- package/src/resources/contents.ts +424 -0
- package/src/resources/deliveries.ts +186 -0
- package/src/resources/oauth.ts +165 -0
- package/src/resources/webhooks.ts +301 -0
- package/src/resources/workspaces.ts +70 -0
- package/src/transport.ts +198 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @outposted/node — public entry point.
|
|
3
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export { Outposted, type OutpostedOptions } from './client';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
OutpostedError,
|
|
9
|
+
OutpostedAuthError,
|
|
10
|
+
OutpostedValidationError,
|
|
11
|
+
OutpostedForbiddenError,
|
|
12
|
+
OutpostedNotFoundError,
|
|
13
|
+
OutpostedConflictError,
|
|
14
|
+
OutpostedRateLimitError,
|
|
15
|
+
OutpostedServerError,
|
|
16
|
+
OutpostedNetworkError,
|
|
17
|
+
type OutpostedErrorOptions,
|
|
18
|
+
type OutpostedErrorBody,
|
|
19
|
+
type ValidationIssue,
|
|
20
|
+
type OutpostedValidationErrorOptions,
|
|
21
|
+
type OutpostedRateLimitErrorOptions,
|
|
22
|
+
} from './errors';
|
|
23
|
+
|
|
24
|
+
export { HttpClient, type HttpClientOptions, type RequestOptions, type FetchLike } from './transport';
|
|
25
|
+
|
|
26
|
+
export { WorkspacesResource } from './resources/workspaces';
|
|
27
|
+
export { BrandsResource } from './resources/brands';
|
|
28
|
+
export { ApiKeysResource } from './resources/api-keys';
|
|
29
|
+
export { ContentsResource } from './resources/contents';
|
|
30
|
+
export { ConnectionsResource } from './resources/connections';
|
|
31
|
+
export { WebhooksResource } from './resources/webhooks';
|
|
32
|
+
export { DeliveriesResource } from './resources/deliveries';
|
|
33
|
+
export { OAuthResource } from './resources/oauth';
|
|
34
|
+
|
|
35
|
+
// Re-export webhook signing helpers so consumers don't have to install a
|
|
36
|
+
// second package to verify incoming Outposted webhooks.
|
|
37
|
+
export { signPayload, verifyPayload } from '@outposted/webhooks';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @outposted/node/resources/api-keys — workspace API key management.
|
|
3
|
+
//
|
|
4
|
+
// Two operations, both scoped to a workspace:
|
|
5
|
+
//
|
|
6
|
+
// - list(workspaceId) GET /v1/workspaces/:wsId/api-keys
|
|
7
|
+
// - rotate(workspaceId, body?) POST /v1/workspaces/:wsId/api-keys
|
|
8
|
+
//
|
|
9
|
+
// The server NEVER returns plaintext on list — only metadata (id, name, prefix,
|
|
10
|
+
// timestamps). Plaintext is returned exactly ONCE, by `rotate()`, on the same
|
|
11
|
+
// 201 response that creates the new key. There is no second chance: if the
|
|
12
|
+
// caller drops the response, the key is unrecoverable.
|
|
13
|
+
//
|
|
14
|
+
// `revoke_others: true` is the proper rotation hygiene: create a new key, then
|
|
15
|
+
// in the SAME atomic operation revoke every other active key in the workspace.
|
|
16
|
+
// Use this on credential leak or scheduled rotation.
|
|
17
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
import type { HttpClient } from '../transport';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Metadata for an API key as returned by `list()`. Contains NO secret material —
|
|
23
|
+
* neither the plaintext key nor its hash. `prefix` is the short, non-secret
|
|
24
|
+
* identifier (e.g. `opst_live_AbCd...`) safe to display in dashboards and logs.
|
|
25
|
+
*/
|
|
26
|
+
export interface ApiKey {
|
|
27
|
+
/** Stable identifier (e.g. `key_01HXY...`). */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Human label set at creation time. Defaults to `"rotated"` when not provided. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Non-secret prefix safe to display (e.g. `opst_live_AbCd...`). */
|
|
32
|
+
prefix: string;
|
|
33
|
+
/** ISO-8601 timestamp the key was created. */
|
|
34
|
+
created_at: string;
|
|
35
|
+
/** ISO-8601 timestamp of the most recent authenticated request, or `null`. */
|
|
36
|
+
last_used_at: string | null;
|
|
37
|
+
/** ISO-8601 timestamp the key was revoked, or `null` if still active. */
|
|
38
|
+
revoked_at: string | null;
|
|
39
|
+
/** ISO-8601 expiry, or `null` if non-expiring. */
|
|
40
|
+
expires_at: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Response from `rotate()`. Carries the freshly-minted plaintext `api_key` —
|
|
45
|
+
* the one and ONLY time the SDK will ever expose it. Persist it immediately;
|
|
46
|
+
* the server cannot return it again.
|
|
47
|
+
*/
|
|
48
|
+
export interface RotatedApiKey {
|
|
49
|
+
/** Stable identifier of the new key. */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Human label (the `name` arg, or `"rotated"` when omitted). */
|
|
52
|
+
name: string;
|
|
53
|
+
/** Non-secret prefix safe to display (e.g. `opst_live_AbCd...`). */
|
|
54
|
+
prefix: string;
|
|
55
|
+
/** **Plaintext key. Returned ONCE. Persist immediately — it cannot be retrieved later.** */
|
|
56
|
+
api_key: string;
|
|
57
|
+
/** ISO-8601 timestamp the key was created. */
|
|
58
|
+
created_at: string;
|
|
59
|
+
/** `true` when `revoke_others: true` was honoured and all other active keys were revoked. */
|
|
60
|
+
revoked_others: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Body for `rotate()`. Both fields are optional; an empty body is valid. */
|
|
64
|
+
export interface RotateApiKeyInput {
|
|
65
|
+
/** Label for the new key (1-100 chars). Defaults server-side to `"rotated"`. */
|
|
66
|
+
name?: string;
|
|
67
|
+
/** When `true`, revoke every OTHER active key in the workspace atomically. */
|
|
68
|
+
revoke_others?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ListResponse {
|
|
72
|
+
data: ApiKey[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* API key resource — list metadata and rotate (create + optionally revoke
|
|
77
|
+
* others) keys for a given workspace. The Bearer key used by the SDK MUST
|
|
78
|
+
* already belong to the workspace it's operating on; the server enforces this.
|
|
79
|
+
*/
|
|
80
|
+
export class ApiKeysResource {
|
|
81
|
+
constructor(private readonly http: HttpClient) {}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List every API key in a workspace — metadata only. Plaintext and hash are
|
|
85
|
+
* NEVER included. Useful for displaying key inventories in dashboards or
|
|
86
|
+
* pruning stale credentials.
|
|
87
|
+
*
|
|
88
|
+
* The response is unwrapped: the SDK returns the `ApiKey[]` array directly,
|
|
89
|
+
* not the `{ data: [...] }` envelope the wire format uses.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```bash
|
|
93
|
+
* curl https://api.outposted.one/v1/workspaces/ws_123/api-keys \
|
|
94
|
+
* -H "Authorization: Bearer opst_live_..."
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* const keys = await client.apiKeys.list('ws_123');
|
|
100
|
+
* for (const k of keys) {
|
|
101
|
+
* console.log(k.prefix, k.last_used_at);
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
async list(workspaceId: string): Promise<ApiKey[]> {
|
|
106
|
+
const response = await this.http.request<ListResponse>({
|
|
107
|
+
method: 'GET',
|
|
108
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/api-keys`,
|
|
109
|
+
});
|
|
110
|
+
return response.data ?? [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Rotate (create) a new API key for a workspace. When `revoke_others: true`,
|
|
115
|
+
* every other active key in the workspace is revoked atomically in the same
|
|
116
|
+
* operation — the canonical rotation flow.
|
|
117
|
+
*
|
|
118
|
+
* **CRITICAL — read the response carefully:** the returned `api_key` field
|
|
119
|
+
* is the PLAINTEXT key, and the server returns it exactly ONCE on this call.
|
|
120
|
+
* The plaintext is never persisted server-side and CANNOT be retrieved
|
|
121
|
+
* again. Persist `result.api_key` to your secret store IMMEDIATELY — before
|
|
122
|
+
* any other operation that could throw — or you'll have to rotate again to
|
|
123
|
+
* recover.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```bash
|
|
127
|
+
* # Simple rotation — keeps existing keys alive.
|
|
128
|
+
* curl -X POST https://api.outposted.one/v1/workspaces/ws_123/api-keys \
|
|
129
|
+
* -H "Authorization: Bearer opst_live_..." \
|
|
130
|
+
* -H "Content-Type: application/json" \
|
|
131
|
+
* -d '{"name":"ci-pipeline"}'
|
|
132
|
+
*
|
|
133
|
+
* # Full rotation — new key + revoke every other active key.
|
|
134
|
+
* curl -X POST https://api.outposted.one/v1/workspaces/ws_123/api-keys \
|
|
135
|
+
* -H "Authorization: Bearer opst_live_..." \
|
|
136
|
+
* -H "Content-Type: application/json" \
|
|
137
|
+
* -d '{"name":"post-leak","revoke_others":true}'
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* const result = await client.apiKeys.rotate('ws_123', {
|
|
143
|
+
* name: 'post-leak',
|
|
144
|
+
* revoke_others: true,
|
|
145
|
+
* });
|
|
146
|
+
* // ⚠️ result.api_key is the plaintext — store it NOW.
|
|
147
|
+
* await secretStore.put('OUTPOSTED_API_KEY', result.api_key);
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
async rotate(workspaceId: string, input: RotateApiKeyInput = {}): Promise<RotatedApiKey> {
|
|
151
|
+
return this.http.request<RotatedApiKey>({
|
|
152
|
+
method: 'POST',
|
|
153
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/api-keys`,
|
|
154
|
+
body: input,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @outposted/node/resources/brands — Brand publishing identities.
|
|
3
|
+
//
|
|
4
|
+
// A Brand is a publishing identity inside a workspace. One brand can connect
|
|
5
|
+
// multiple platforms (e.g. 1 IG handle + 1 FB Page + 1 Threads). The slug is
|
|
6
|
+
// unique per workspace.
|
|
7
|
+
//
|
|
8
|
+
// Endpoints proxied here (see apps/api/src/app/api/v1/workspaces/[wsId]/brands):
|
|
9
|
+
// - GET /v1/workspaces/:wsId/brands → list
|
|
10
|
+
// - POST /v1/workspaces/:wsId/brands → create
|
|
11
|
+
// - GET /v1/workspaces/:wsId/brands/:brandId → fetch one (with connections)
|
|
12
|
+
//
|
|
13
|
+
// All methods unwrap the API envelope (`{ brand }` / `{ brands }`) so the
|
|
14
|
+
// caller gets the domain entity directly. Errors are surfaced as the typed
|
|
15
|
+
// OutpostedError subclasses produced by the transport layer.
|
|
16
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
import type { Brand, BrandCreateInput } from '@outposted/domain';
|
|
19
|
+
import { OutpostedNotFoundError } from '../errors';
|
|
20
|
+
import type { HttpClient } from '../transport';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Payload accepted by `BrandsResource.create`. Mirrors the POST body the API
|
|
24
|
+
* expects — `workspace_id` is taken from the URL, so we omit it from the body
|
|
25
|
+
* shape exposed by the SDK.
|
|
26
|
+
*/
|
|
27
|
+
export type BrandCreateBody = Omit<BrandCreateInput, 'workspace_id'>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* SDK accessor for the `/v1/workspaces/:wsId/brands` collection.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
|
|
35
|
+
* const brands = await outposted.brands.list('ws_123');
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class BrandsResource {
|
|
39
|
+
constructor(private readonly http: HttpClient) {}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List every brand in a workspace, newest first.
|
|
43
|
+
*
|
|
44
|
+
* @example curl
|
|
45
|
+
* ```bash
|
|
46
|
+
* curl -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
47
|
+
* https://api.outposted.one/v1/workspaces/ws_123/brands
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example SDK
|
|
51
|
+
* ```ts
|
|
52
|
+
* const brands = await outposted.brands.list('ws_123');
|
|
53
|
+
* console.log(brands.map((b) => b.slug));
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
async list(workspaceId: string): Promise<Brand[]> {
|
|
57
|
+
const { brands } = await this.http.request<{ brands: Brand[] }>({
|
|
58
|
+
method: 'GET',
|
|
59
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/brands`,
|
|
60
|
+
});
|
|
61
|
+
return brands;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a brand inside a workspace. Slugs are unique per workspace; a
|
|
66
|
+
* conflicting slug surfaces as `OutpostedConflictError` (HTTP 409).
|
|
67
|
+
*
|
|
68
|
+
* @example curl
|
|
69
|
+
* ```bash
|
|
70
|
+
* curl -X POST -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
71
|
+
* -H "Content-Type: application/json" \
|
|
72
|
+
* -d '{"name":"Coke","slug":"coke","brand_type":"product"}' \
|
|
73
|
+
* https://api.outposted.one/v1/workspaces/ws_123/brands
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example SDK
|
|
77
|
+
* ```ts
|
|
78
|
+
* const brand = await outposted.brands.create('ws_123', {
|
|
79
|
+
* name: 'Coke',
|
|
80
|
+
* slug: 'coke',
|
|
81
|
+
* brand_type: 'product',
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
async create(workspaceId: string, input: BrandCreateBody): Promise<Brand> {
|
|
86
|
+
const { brand } = await this.http.request<{ brand: Brand }>({
|
|
87
|
+
method: 'POST',
|
|
88
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/brands`,
|
|
89
|
+
body: input,
|
|
90
|
+
});
|
|
91
|
+
return brand;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fetch a single brand by id. The API responds with the brand row plus its
|
|
96
|
+
* nested `connected_accounts` — the SDK returns the unwrapped object.
|
|
97
|
+
*
|
|
98
|
+
* Throws `OutpostedNotFoundError` (HTTP 404) when the brand does not exist
|
|
99
|
+
* or does not belong to the authenticated workspace.
|
|
100
|
+
*
|
|
101
|
+
* @example curl
|
|
102
|
+
* ```bash
|
|
103
|
+
* curl -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
104
|
+
* https://api.outposted.one/v1/workspaces/ws_123/brands/brand_abc
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @example SDK
|
|
108
|
+
* ```ts
|
|
109
|
+
* const brand = await outposted.brands.get('ws_123', 'brand_abc');
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
async get(workspaceId: string, brandId: string): Promise<Brand> {
|
|
113
|
+
const { brand } = await this.http.request<{ brand: Brand | null }>({
|
|
114
|
+
method: 'GET',
|
|
115
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/brands/${encodeURIComponent(brandId)}`,
|
|
116
|
+
});
|
|
117
|
+
if (!brand) {
|
|
118
|
+
// Defense in depth — the API itself returns 404 for missing brands, but
|
|
119
|
+
// if it ever returns 200 with a null body we still want a typed error.
|
|
120
|
+
throw new OutpostedNotFoundError(`brand ${brandId} not found in workspace ${workspaceId}`, {
|
|
121
|
+
status: 404,
|
|
122
|
+
code: 'brand_not_found',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return brand;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @outposted/node/resources/connections — ConnectedAccount resource.
|
|
3
|
+
//
|
|
4
|
+
// A `connected_account` ties a brand to one external account on one platform
|
|
5
|
+
// (e.g. brand `coke` → instagram → ig_user_id 1234). They're created
|
|
6
|
+
// automatically by the OAuth flow: the user is redirected to the platform,
|
|
7
|
+
// authorizes, and the callback handler persists the encrypted token + handle
|
|
8
|
+
// before bouncing back to the app. The SDK exposes the entry point of that
|
|
9
|
+
// flow (`getOAuthStartUrl`) so server-side integrations can mint redirect
|
|
10
|
+
// URLs without going through a UI.
|
|
11
|
+
//
|
|
12
|
+
// `list()` is a stub today — the V0 API surface doesn't include a list
|
|
13
|
+
// endpoint for connected accounts. When one lands (planned route:
|
|
14
|
+
// `GET /api/v1/workspaces/:wsId/brands/:brandId/connections`), wire it up
|
|
15
|
+
// here. Until then, calling `list()` throws so consumers fail loud instead
|
|
16
|
+
// of silently getting an empty array.
|
|
17
|
+
//
|
|
18
|
+
// TODO(api): wire `list()` to `GET /api/v1/workspaces/:wsId/brands/:brandId/connections`
|
|
19
|
+
// once that route exists in `apps/api/src/app/api/v1/workspaces/[wsId]/brands/[brandId]/connections/route.ts`.
|
|
20
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
import type { ConnectedAccount, AccountType } from '@outposted/domain';
|
|
23
|
+
import type { HttpClient } from '../transport';
|
|
24
|
+
|
|
25
|
+
/** Platforms the OAuth start endpoint accepts. Mirrors `StartSchema.platform` in `connections/start/route.ts`. */
|
|
26
|
+
export type ConnectionPlatform =
|
|
27
|
+
| 'instagram'
|
|
28
|
+
| 'facebook'
|
|
29
|
+
| 'threads'
|
|
30
|
+
| 'youtube'
|
|
31
|
+
| 'linkedin';
|
|
32
|
+
|
|
33
|
+
export interface GetOAuthStartUrlOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Optional URL the OAuth callback will redirect the user to after the
|
|
36
|
+
* connection is persisted. Only honored if it's an absolute URL pointing
|
|
37
|
+
* to a host the backend trusts; otherwise the backend uses the default
|
|
38
|
+
* post-connect destination.
|
|
39
|
+
*/
|
|
40
|
+
returnTo?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ConnectionsResource {
|
|
44
|
+
constructor(private readonly http: HttpClient) {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List the connected accounts attached to a brand.
|
|
48
|
+
*
|
|
49
|
+
* **Not implemented in this API version.** The V0 API exposes the OAuth
|
|
50
|
+
* start endpoint (see `getOAuthStartUrl`) and the callback handler that
|
|
51
|
+
* creates rows, but does not yet ship a read endpoint. This method exists
|
|
52
|
+
* so the resource shape is forward-compatible — calling it today throws.
|
|
53
|
+
*
|
|
54
|
+
* Equivalent (future) curl:
|
|
55
|
+
* ```bash
|
|
56
|
+
* curl https://api.outposted.one/api/v1/workspaces/$WS_ID/brands/$BRAND_ID/connections \
|
|
57
|
+
* -H "Authorization: Bearer $OUTPOSTED_API_KEY"
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Throws `Error('Not implemented in this API version — use the OAuth start URL flow to add connections.')`.
|
|
61
|
+
*/
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
63
|
+
async list(workspaceId: string, brandId: string): Promise<ConnectedAccount[]> {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'Not implemented in this API version — use the OAuth start URL flow to add connections.',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the URL the end user should be redirected to in order to authorize
|
|
71
|
+
* a connection. Does NOT make an HTTP call — pure URL construction.
|
|
72
|
+
*
|
|
73
|
+
* Flow:
|
|
74
|
+
* 1. Your server redirects the end user to this URL.
|
|
75
|
+
* 2. They authorize on the platform (Instagram / Facebook / etc.).
|
|
76
|
+
* 3. The platform redirects to Outposted's OAuth callback, which exchanges
|
|
77
|
+
* the code for a token, encrypts it, and persists a `ConnectedAccount`
|
|
78
|
+
* row automatically.
|
|
79
|
+
* 4. The user bounces back to your app (default destination, or the URL
|
|
80
|
+
* passed via `returnTo` if the backend trusts it).
|
|
81
|
+
*
|
|
82
|
+
* SDK call:
|
|
83
|
+
* ```ts
|
|
84
|
+
* const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
|
|
85
|
+
* const url = outposted.connections.getOAuthStartUrl('ws_123', 'brand_456', 'instagram', {
|
|
86
|
+
* returnTo: 'https://app.example.com/settings/connections',
|
|
87
|
+
* });
|
|
88
|
+
* // redirect the end user there; they authorize on the platform; the
|
|
89
|
+
* // OAuth callback creates the ConnectedAccount automatically.
|
|
90
|
+
* res.redirect(url);
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @param workspaceId Workspace that owns the brand.
|
|
94
|
+
* @param brandId Brand the new connection will be attached to.
|
|
95
|
+
* @param platform Target platform (`instagram` | `facebook` | `threads` | `youtube` | `linkedin`).
|
|
96
|
+
* @param opts Optional flags — currently just `returnTo`.
|
|
97
|
+
*/
|
|
98
|
+
getOAuthStartUrl(
|
|
99
|
+
workspaceId: string,
|
|
100
|
+
brandId: string,
|
|
101
|
+
platform: ConnectionPlatform,
|
|
102
|
+
opts?: GetOAuthStartUrlOptions,
|
|
103
|
+
): string {
|
|
104
|
+
const url = new URL(`${this.http.baseUrl}/api/v1/oauth/${platform}/start`);
|
|
105
|
+
url.searchParams.set('brand_id', brandId);
|
|
106
|
+
url.searchParams.set('workspace_id', workspaceId);
|
|
107
|
+
if (opts?.returnTo) {
|
|
108
|
+
url.searchParams.set('return_to', opts.returnTo);
|
|
109
|
+
}
|
|
110
|
+
return url.toString();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Re-export the domain types so consumers don't need a second import.
|
|
115
|
+
export type { ConnectedAccount, AccountType };
|