@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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @outposted/node/resources/oauth — OAuth start URL builder.
|
|
3
|
+
//
|
|
4
|
+
// This resource is intentionally **pure**: every method returns a URL string
|
|
5
|
+
// and performs NO HTTP I/O. The customer redirects the end user's browser to
|
|
6
|
+
// the returned URL; the user authorizes on the platform's site; the OAuth
|
|
7
|
+
// callback inside Outposted API (see apps/api/src/app/api/v1/oauth/*/callback)
|
|
8
|
+
// completes the handshake and persists the ConnectedAccount.
|
|
9
|
+
//
|
|
10
|
+
// Shape (per platform):
|
|
11
|
+
// `${baseUrl}/api/v1/oauth/{platform}/start?brand_id=...&[other params]`
|
|
12
|
+
//
|
|
13
|
+
// Notes:
|
|
14
|
+
// - `brand_id` is always required — a ConnectedAccount belongs to a brand.
|
|
15
|
+
// - `workspace_id` is optional. When omitted, the API resolves it from the
|
|
16
|
+
// authenticated session/API key.
|
|
17
|
+
// - `return_to` is optional. After a successful callback, Outposted will
|
|
18
|
+
// redirect the browser to this URL. Caller passes a raw URL string; the
|
|
19
|
+
// SDK URL-encodes it.
|
|
20
|
+
// - LinkedIn supports `account_type` (`personal` | `page`) to pre-select the
|
|
21
|
+
// identity flow on the LinkedIn authorize screen.
|
|
22
|
+
//
|
|
23
|
+
// Why a URL builder and not a `POST /start` wrapper:
|
|
24
|
+
// The OAuth start endpoints expose GET semantics — the call IS the redirect.
|
|
25
|
+
// Wrapping them in HTTP would mean fetching the redirect ourselves and
|
|
26
|
+
// re-emitting it, which doesn't work for browser-driven OAuth flows.
|
|
27
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
import type { HttpClient } from '../transport';
|
|
30
|
+
|
|
31
|
+
/** Common args shared across every `start*` method. */
|
|
32
|
+
interface OAuthStartArgs {
|
|
33
|
+
/** Brand the resulting ConnectedAccount will belong to. Required. */
|
|
34
|
+
brandId: string;
|
|
35
|
+
/** Workspace scope. Optional — the API resolves it from the API key when omitted. */
|
|
36
|
+
workspaceId?: string;
|
|
37
|
+
/** Absolute URL the browser is redirected to after a successful callback. */
|
|
38
|
+
returnTo?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** LinkedIn-specific args: pre-select personal vs page identity. */
|
|
42
|
+
export interface OAuthLinkedInStartArgs extends OAuthStartArgs {
|
|
43
|
+
/**
|
|
44
|
+
* Pre-select the LinkedIn identity type at the authorize screen.
|
|
45
|
+
* - `personal` — connect as the authenticated member.
|
|
46
|
+
* - `page` — connect as a Company Page the member administers.
|
|
47
|
+
*/
|
|
48
|
+
accountType?: 'personal' | 'page';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds OAuth start URLs for every supported platform.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import express from 'express';
|
|
57
|
+
* import { Outposted } from '@outposted/node';
|
|
58
|
+
*
|
|
59
|
+
* const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
|
|
60
|
+
* const app = express();
|
|
61
|
+
*
|
|
62
|
+
* app.get('/connect/instagram', (req, res) => {
|
|
63
|
+
* const url = outposted.oauth.startInstagram({ brandId: 'brand_abc' });
|
|
64
|
+
* res.redirect(url);
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export class OAuthResource {
|
|
69
|
+
constructor(private readonly http: HttpClient) {}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the Instagram OAuth start URL.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const url = client.oauth.startInstagram({ brandId: 'brand_abc' });
|
|
77
|
+
* res.redirect(url);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
startInstagram(args: OAuthStartArgs): string {
|
|
81
|
+
return this.build('instagram', args);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the Facebook OAuth start URL.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const url = client.oauth.startFacebook({
|
|
90
|
+
* brandId: 'brand_abc',
|
|
91
|
+
* returnTo: 'https://app.example.com/connected',
|
|
92
|
+
* });
|
|
93
|
+
* res.redirect(url);
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
startFacebook(args: OAuthStartArgs): string {
|
|
97
|
+
return this.build('facebook', args);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build the Threads OAuth start URL.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const url = client.oauth.startThreads({ brandId: 'brand_abc' });
|
|
106
|
+
* res.redirect(url);
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
startThreads(args: OAuthStartArgs): string {
|
|
110
|
+
return this.build('threads', args);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build the YouTube OAuth start URL.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const url = client.oauth.startYouTube({ brandId: 'brand_abc' });
|
|
119
|
+
* res.redirect(url);
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
startYouTube(args: OAuthStartArgs): string {
|
|
123
|
+
return this.build('youtube', args);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build the LinkedIn OAuth start URL. Pass `accountType` to pre-select the
|
|
128
|
+
* personal vs page authorize flow.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* const url = client.oauth.startLinkedIn({
|
|
133
|
+
* brandId: 'brand_abc',
|
|
134
|
+
* accountType: 'page',
|
|
135
|
+
* });
|
|
136
|
+
* res.redirect(url);
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
startLinkedIn(args: OAuthLinkedInStartArgs): string {
|
|
140
|
+
const url = new URL(`${this.http.baseUrl}/api/v1/oauth/linkedin/start`);
|
|
141
|
+
appendCommon(url, args);
|
|
142
|
+
if (args.accountType !== undefined) {
|
|
143
|
+
url.searchParams.set('account_type', args.accountType);
|
|
144
|
+
}
|
|
145
|
+
return url.toString();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── internal ─────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
private build(platform: 'instagram' | 'facebook' | 'threads' | 'youtube', args: OAuthStartArgs): string {
|
|
151
|
+
const url = new URL(`${this.http.baseUrl}/api/v1/oauth/${platform}/start`);
|
|
152
|
+
appendCommon(url, args);
|
|
153
|
+
return url.toString();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function appendCommon(url: URL, args: OAuthStartArgs): void {
|
|
158
|
+
url.searchParams.set('brand_id', args.brandId);
|
|
159
|
+
if (args.workspaceId !== undefined) {
|
|
160
|
+
url.searchParams.set('workspace_id', args.workspaceId);
|
|
161
|
+
}
|
|
162
|
+
if (args.returnTo !== undefined) {
|
|
163
|
+
url.searchParams.set('return_to', args.returnTo);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @outposted/node/resources/webhooks — outbound webhook endpoints (E2).
|
|
3
|
+
//
|
|
4
|
+
// A webhook endpoint is a HTTPS URL registered against a workspace that
|
|
5
|
+
// receives signed POSTs when domain events fire (content.published,
|
|
6
|
+
// content.failed, …). The workspace's `whsec_*` secret is generated server-side
|
|
7
|
+
// at create-time and returned ONCE in the response body — losing it means
|
|
8
|
+
// rotating to a new endpoint. The DB only stores the ciphertext.
|
|
9
|
+
//
|
|
10
|
+
// Endpoints proxied here (see apps/api/src/app/api/v1/workspaces/[wsId]/webhooks):
|
|
11
|
+
// - POST /v1/workspaces/:wsId/webhooks → register endpoint
|
|
12
|
+
// - GET /v1/workspaces/:wsId/webhooks → list endpoints
|
|
13
|
+
// - GET /v1/workspaces/:wsId/webhooks/:id → fetch one
|
|
14
|
+
// - PATCH /v1/workspaces/:wsId/webhooks/:id → toggle/update
|
|
15
|
+
// - DELETE /v1/workspaces/:wsId/webhooks/:id → hard delete
|
|
16
|
+
//
|
|
17
|
+
// `verify()` delegates straight to `@outposted/webhooks/verifyPayload` — it's
|
|
18
|
+
// the same code path the API + worker use to sign on the way out, so customers
|
|
19
|
+
// can reuse it for inbound verification without installing a second package.
|
|
20
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
import { verifyPayload, type VerifyResult } from '@outposted/webhooks';
|
|
23
|
+
import type { HttpClient } from '../transport';
|
|
24
|
+
|
|
25
|
+
export type { VerifyResult };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Payload accepted by `WebhooksResource.create`. Only `url` is required —
|
|
29
|
+
* `description` is a free-form label and `enabled_events` is the subscription
|
|
30
|
+
* filter (omit/null = receive every event type).
|
|
31
|
+
*/
|
|
32
|
+
export interface CreateWebhookInput {
|
|
33
|
+
/** Absolute HTTPS URL the worker should POST to (HTTP allowed in dev only). */
|
|
34
|
+
url: string;
|
|
35
|
+
/** Optional human-readable label (max 200 chars). */
|
|
36
|
+
description?: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Event types the endpoint subscribes to (e.g. `['content.published']`).
|
|
39
|
+
* Omit or pass null to receive every event type the workspace emits.
|
|
40
|
+
*/
|
|
41
|
+
enabled_events?: string[] | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Webhook endpoint as returned by list/get/update. Does NOT contain the
|
|
46
|
+
* plaintext secret — the API never returns it after creation.
|
|
47
|
+
*/
|
|
48
|
+
export interface Webhook {
|
|
49
|
+
id: string;
|
|
50
|
+
workspace_id: string;
|
|
51
|
+
url: string;
|
|
52
|
+
description: string | null;
|
|
53
|
+
enabled_events: string[] | null;
|
|
54
|
+
active: boolean;
|
|
55
|
+
last_delivery_at?: string | null;
|
|
56
|
+
last_delivery_status?: string | null;
|
|
57
|
+
created_at: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Response from `WebhooksResource.create`. Identical to `Webhook` but with the
|
|
62
|
+
* plaintext `secret` field populated — capture it immediately, it will never
|
|
63
|
+
* appear in any other response.
|
|
64
|
+
*/
|
|
65
|
+
export interface CreatedWebhook extends Webhook {
|
|
66
|
+
/**
|
|
67
|
+
* Plaintext signing secret in `whsec_...` shape. Shown exactly ONCE at
|
|
68
|
+
* creation — the API stores only the encrypted form. Save it to your secret
|
|
69
|
+
* manager before discarding the response object.
|
|
70
|
+
*/
|
|
71
|
+
secret: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Patch body accepted by `WebhooksResource.update`. Every field is optional;
|
|
76
|
+
* at least one must be provided or the API responds 400.
|
|
77
|
+
*/
|
|
78
|
+
export interface UpdateWebhookInput {
|
|
79
|
+
url?: string;
|
|
80
|
+
description?: string | null;
|
|
81
|
+
enabled_events?: string[] | null;
|
|
82
|
+
active?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Options for `WebhooksResource.verify`. Forwarded straight to
|
|
87
|
+
* `verifyPayload` from `@outposted/webhooks`.
|
|
88
|
+
*/
|
|
89
|
+
export interface VerifyOptions {
|
|
90
|
+
/** Replay window in seconds (default 300 = 5 min, Stripe-compatible). */
|
|
91
|
+
toleranceSeconds?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SDK accessor for the `/v1/workspaces/:wsId/webhooks` collection.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
|
|
100
|
+
* const endpoint = await outposted.webhooks.create('ws_123', {
|
|
101
|
+
* url: 'https://api.example.com/outposted-webhook',
|
|
102
|
+
* enabled_events: ['content.published', 'content.failed'],
|
|
103
|
+
* });
|
|
104
|
+
* console.log(endpoint.secret); // 'whsec_...' — store this NOW
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export class WebhooksResource {
|
|
108
|
+
constructor(private readonly http: HttpClient) {}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register a webhook endpoint inside a workspace. The API generates a fresh
|
|
112
|
+
* `whsec_*` secret server-side and returns it in the response body —
|
|
113
|
+
* **this is the only time the plaintext appears**, so capture it before
|
|
114
|
+
* discarding the response. Subsequent list/get calls never expose it.
|
|
115
|
+
*
|
|
116
|
+
* Throws `OutpostedValidationError` (400) on bad URL or unknown event types,
|
|
117
|
+
* and `OutpostedConflictError` (409) once the workspace hits the 25-endpoint
|
|
118
|
+
* cap.
|
|
119
|
+
*
|
|
120
|
+
* @example curl
|
|
121
|
+
* ```bash
|
|
122
|
+
* curl -X POST -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
123
|
+
* -H "Content-Type: application/json" \
|
|
124
|
+
* -d '{"url":"https://example.com/hook","enabled_events":["content.published"]}' \
|
|
125
|
+
* https://api.outposted.one/api/v1/workspaces/ws_123/webhooks
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @example SDK
|
|
129
|
+
* ```ts
|
|
130
|
+
* const endpoint = await outposted.webhooks.create('ws_123', {
|
|
131
|
+
* url: 'https://example.com/hook',
|
|
132
|
+
* enabled_events: ['content.published'],
|
|
133
|
+
* });
|
|
134
|
+
* await secretManager.store('OUTPOSTED_WHSEC', endpoint.secret); // ONLY chance
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
async create(workspaceId: string, input: CreateWebhookInput): Promise<CreatedWebhook> {
|
|
138
|
+
return this.http.request<CreatedWebhook>({
|
|
139
|
+
method: 'POST',
|
|
140
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks`,
|
|
141
|
+
body: input,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* List every webhook endpoint registered against the workspace. The plaintext
|
|
147
|
+
* secret is NEVER included — it only exists in the `create` response.
|
|
148
|
+
*
|
|
149
|
+
* @example curl
|
|
150
|
+
* ```bash
|
|
151
|
+
* curl -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
152
|
+
* https://api.outposted.one/api/v1/workspaces/ws_123/webhooks
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @example SDK
|
|
156
|
+
* ```ts
|
|
157
|
+
* const endpoints = await outposted.webhooks.list('ws_123');
|
|
158
|
+
* for (const ep of endpoints) {
|
|
159
|
+
* console.log(ep.url, ep.active);
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
async list(workspaceId: string): Promise<Webhook[]> {
|
|
164
|
+
const { data } = await this.http.request<{ data: Webhook[] }>({
|
|
165
|
+
method: 'GET',
|
|
166
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks`,
|
|
167
|
+
});
|
|
168
|
+
return data;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fetch a single webhook endpoint by id. Throws `OutpostedNotFoundError`
|
|
173
|
+
* (404) when the endpoint doesn't exist or belongs to a different workspace.
|
|
174
|
+
*
|
|
175
|
+
* @example curl
|
|
176
|
+
* ```bash
|
|
177
|
+
* curl -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
178
|
+
* https://api.outposted.one/api/v1/workspaces/ws_123/webhooks/whep_abc
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* @example SDK
|
|
182
|
+
* ```ts
|
|
183
|
+
* const endpoint = await outposted.webhooks.get('ws_123', 'whep_abc');
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
async get(workspaceId: string, webhookId: string): Promise<Webhook> {
|
|
187
|
+
return this.http.request<Webhook>({
|
|
188
|
+
method: 'GET',
|
|
189
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(webhookId)}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Patch an existing endpoint. At least one field must be present in `patch`
|
|
195
|
+
* — sending an empty object responds 400. Toggling `active: false` pauses
|
|
196
|
+
* deliveries without losing the endpoint row.
|
|
197
|
+
*
|
|
198
|
+
* @example curl
|
|
199
|
+
* ```bash
|
|
200
|
+
* curl -X PATCH -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
201
|
+
* -H "Content-Type: application/json" \
|
|
202
|
+
* -d '{"active":false}' \
|
|
203
|
+
* https://api.outposted.one/api/v1/workspaces/ws_123/webhooks/whep_abc
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @example SDK
|
|
207
|
+
* ```ts
|
|
208
|
+
* await outposted.webhooks.update('ws_123', 'whep_abc', { active: false });
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
async update(workspaceId: string, webhookId: string, patch: UpdateWebhookInput): Promise<Webhook> {
|
|
212
|
+
return this.http.request<Webhook>({
|
|
213
|
+
method: 'PATCH',
|
|
214
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(webhookId)}`,
|
|
215
|
+
body: patch,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Hard-delete an endpoint. Idempotent from the caller's perspective —
|
|
221
|
+
* deleting an already-gone endpoint responds 404 (surfaced as
|
|
222
|
+
* `OutpostedNotFoundError`).
|
|
223
|
+
*
|
|
224
|
+
* @example curl
|
|
225
|
+
* ```bash
|
|
226
|
+
* curl -X DELETE -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
|
|
227
|
+
* https://api.outposted.one/api/v1/workspaces/ws_123/webhooks/whep_abc
|
|
228
|
+
* ```
|
|
229
|
+
*
|
|
230
|
+
* @example SDK
|
|
231
|
+
* ```ts
|
|
232
|
+
* await outposted.webhooks.delete('ws_123', 'whep_abc');
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
async delete(workspaceId: string, webhookId: string): Promise<void> {
|
|
236
|
+
await this.http.request<undefined>({
|
|
237
|
+
method: 'DELETE',
|
|
238
|
+
path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(webhookId)}`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Verify an inbound signed delivery using the workspace's `whsec_*` secret
|
|
244
|
+
* (the one returned ONCE by `create`). Delegates to
|
|
245
|
+
* `verifyPayload` from `@outposted/webhooks` so the verification code is
|
|
246
|
+
* byte-identical to the API + worker signing path.
|
|
247
|
+
*
|
|
248
|
+
* Returns a discriminated union — branch on `result.ok` and `result.code` to
|
|
249
|
+
* respond with the right HTTP status:
|
|
250
|
+
* - `ok: true` → 200 OK, process the event
|
|
251
|
+
* - `code: 'missing' | 'malformed'` → 400 Bad Request
|
|
252
|
+
* - `code: 'unsupported_version'` → 400 Bad Request
|
|
253
|
+
* - `code: 'timestamp_out_of_tolerance'` → 408 Request Timeout
|
|
254
|
+
* - `code: 'signature_mismatch'` → 401 Unauthorized
|
|
255
|
+
*
|
|
256
|
+
* `rawBody` MUST be the exact bytes of the request body — any reformatting
|
|
257
|
+
* (key re-ordering, whitespace normalization) breaks the HMAC. Capture the
|
|
258
|
+
* raw stream BEFORE any JSON parser touches it.
|
|
259
|
+
*
|
|
260
|
+
* @example Express middleware
|
|
261
|
+
* ```ts
|
|
262
|
+
* import express from 'express';
|
|
263
|
+
* import { Outposted } from '@outposted/node';
|
|
264
|
+
*
|
|
265
|
+
* const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
|
|
266
|
+
* const secret = process.env.OUTPOSTED_WHSEC!; // saved from webhooks.create()
|
|
267
|
+
*
|
|
268
|
+
* const app = express();
|
|
269
|
+
*
|
|
270
|
+
* // raw body capture — this MUST come before any JSON middleware
|
|
271
|
+
* app.post(
|
|
272
|
+
* '/outposted-webhook',
|
|
273
|
+
* express.raw({ type: 'application/json' }),
|
|
274
|
+
* (req, res) => {
|
|
275
|
+
* const rawBody = req.body.toString('utf8');
|
|
276
|
+
* const sig = req.header('x-outposted-signature');
|
|
277
|
+
* const result = outposted.webhooks.verify(rawBody, sig, secret);
|
|
278
|
+
*
|
|
279
|
+
* if (!result.ok) {
|
|
280
|
+
* const status =
|
|
281
|
+
* result.code === 'signature_mismatch' ? 401 :
|
|
282
|
+
* result.code === 'timestamp_out_of_tolerance' ? 408 : 400;
|
|
283
|
+
* return res.status(status).json({ error: result.code, detail: result.detail });
|
|
284
|
+
* }
|
|
285
|
+
*
|
|
286
|
+
* const event = JSON.parse(rawBody);
|
|
287
|
+
* // … process event …
|
|
288
|
+
* res.status(200).end();
|
|
289
|
+
* },
|
|
290
|
+
* );
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
verify(
|
|
294
|
+
rawBody: string,
|
|
295
|
+
headerValue: string | null | undefined,
|
|
296
|
+
secret: string,
|
|
297
|
+
options: VerifyOptions = {},
|
|
298
|
+
): VerifyResult {
|
|
299
|
+
return verifyPayload(rawBody, headerValue, secret, options);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @outposted/node/resources/workspaces — Workspace resource.
|
|
3
|
+
//
|
|
4
|
+
// Wraps `GET /api/v1/workspaces/me`: returns the workspace tied to the
|
|
5
|
+
// authenticated API key. This is the canonical "who am I?" probe — handy as
|
|
6
|
+
// a startup health check that the SDK is configured with a valid key.
|
|
7
|
+
//
|
|
8
|
+
// The API envelope is `{ workspace, api_key_id, rate_limit }`; this resource
|
|
9
|
+
// unwraps it so callers get a clean `Workspace` object. The other envelope
|
|
10
|
+
// fields (api_key_id, rate_limit) are reachable via the standard rate-limit
|
|
11
|
+
// headers and are not surfaced here to keep the resource minimal.
|
|
12
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
import type { HttpClient } from '../transport';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Workspace tied to an API key. Shape mirrors the columns returned by
|
|
18
|
+
* `GET /api/v1/workspaces/me`.
|
|
19
|
+
*/
|
|
20
|
+
export interface Workspace {
|
|
21
|
+
id: string;
|
|
22
|
+
slug: string;
|
|
23
|
+
name: string;
|
|
24
|
+
stripe_customer_id: string | null;
|
|
25
|
+
currency: string;
|
|
26
|
+
created_at: string;
|
|
27
|
+
trial_ends_at: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface WorkspaceMeResponse {
|
|
31
|
+
workspace: Workspace;
|
|
32
|
+
api_key_id: string;
|
|
33
|
+
rate_limit: {
|
|
34
|
+
limit: number;
|
|
35
|
+
remaining: number;
|
|
36
|
+
reset_at: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class WorkspacesResource {
|
|
41
|
+
constructor(private readonly http: HttpClient) {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetch the workspace bound to the current API key.
|
|
45
|
+
*
|
|
46
|
+
* Equivalent curl:
|
|
47
|
+
* ```bash
|
|
48
|
+
* curl https://api.outposted.one/api/v1/workspaces/me \
|
|
49
|
+
* -H "Authorization: Bearer $OUTPOSTED_API_KEY"
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* SDK call:
|
|
53
|
+
* ```ts
|
|
54
|
+
* const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
|
|
55
|
+
* const workspace = await outposted.workspaces.me();
|
|
56
|
+
* console.log(workspace.slug); // "acme"
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* Throws `OutpostedAuthError` (401) if the key is invalid, or
|
|
60
|
+
* `OutpostedNotFoundError` (404) if the workspace row was deleted while the
|
|
61
|
+
* key still exists.
|
|
62
|
+
*/
|
|
63
|
+
async me(): Promise<Workspace> {
|
|
64
|
+
const response = await this.http.request<WorkspaceMeResponse>({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
path: '/api/v1/workspaces/me',
|
|
67
|
+
});
|
|
68
|
+
return response.workspace;
|
|
69
|
+
}
|
|
70
|
+
}
|