@shuffl/sdk 0.3.0 → 0.5.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 CHANGED
@@ -10,7 +10,7 @@ The package is ESM-only and ships its own types. It installs `@shuffl/api-contra
10
10
 
11
11
  Create an API key in **Settings → API keys**. The key is bound to one team and the issuing user's current permissions, and expires within 30 days. There is one kind of API key: the same key works for the SDK, CLI, HTTP API and, when it carries the `mcp.knowledge` permission, the MCP server.
12
12
 
13
- Choose exact endpoint permissions such as `users.list` and `units.save`, grouped by resource and Read/Write. Discover them with `shuffl.permissions.list()`. A write grant does not imply reads or sibling operations. See [endpoint permissions and delegation](https://shuffl-rebuild.vercel.app/docs/api-permissions).
13
+ Choose exact endpoint permissions such as `users.list` and `groups.save`, grouped by resource and Read/Write. Discover them with `shuffl.permissions.list()`. A write grant does not imply reads or sibling operations. See [endpoint permissions and delegation](https://shuffl-rebuild.vercel.app/docs/api-permissions).
14
14
 
15
15
  ```ts
16
16
  import { createShuffl } from '@shuffl/sdk';
@@ -33,7 +33,7 @@ const preview = await shuffl.programs.preview({
33
33
 
34
34
  `users` means people in the team directory, including people who have no web login. IDs are Shuffl person IDs, not Slack IDs or authentication account IDs.
35
35
 
36
- The client includes people and their profiles, teams and departments, access, identity links, programs and runs, Knowledge, invitations, People sync, onboarding and first 90 days check-ins, celebrations, Pulse, Praise, badges, communities, reminders, Home, the HR digest, HR requests, support, reports, exports, credentials and assistant conversations. Company settings and billing are not in the API: a team key never gains company scope. See the [permission reference](https://shuffl-rebuild.vercel.app/docs/api-permissions) for every method. Input and output types come directly from the portable public oRPC contract.
36
+ The client includes people and their profiles, teams and departments, access, identity links, programs and runs, Knowledge, invitations, People sync, onboarding and first 90 days check-ins, celebrations, Pulse, Praise, badges, communities, reminders, Home, the HR digest, HR requests, support, reports, exports, credentials and assistant conversations. A team key never gains company scope; company settings have their own client and key, below. See the [permission reference](https://shuffl-rebuild.vercel.app/docs/api-permissions) for every method. Input and output types come directly from the portable public oRPC contract.
37
37
 
38
38
  Program creation requires a stable `requestKey`. Repeating the same creation request returns the existing program; using that key with different settings returns `CONFLICT`. Programs start paused unless `draft: false` is explicitly supplied. Updates require `expectedRevision`. Run requests require a preview and request key and return a durable run ID; poll `runs.get` for status. The application scheduler owns execution and delivery.
39
39
 
@@ -45,19 +45,65 @@ createShuffl({ baseUrl, token: async () => credentialStore.read() });
45
45
 
46
46
  Use `ORPCError.code` for errors (`UNAUTHORIZED`, `FORBIDDEN`, `BAD_REQUEST`, `NOT_FOUND`, `CONFLICT`, and others). Calls accept oRPC's second options argument, including an abort signal. HTTPS is required except on loopback; redirects are refused to avoid forwarding credentials to another origin. The public API currently allows same-origin browser calls and server/CLI clients; cross-origin browser CORS is not enabled.
47
47
 
48
+ ## Webhooks
49
+
50
+ Shuffl can call your HTTPS endpoint when something happens in a workspace (#643). Add an endpoint in Settings → Webhooks or with `webhooks.create`, keep the `whsec_…` signing secret it returns, and check every delivery against it before you trust it. Pass the raw body, before any JSON parsing:
51
+
52
+ ```ts
53
+ import { verifyWebhook, WebhookVerificationError } from '@shuffl/sdk';
54
+
55
+ export async function POST(request: Request) {
56
+ const body = await request.text();
57
+ try {
58
+ const event = await verifyWebhook({
59
+ body,
60
+ headers: request.headers,
61
+ secret: process.env.SHUFFL_WEBHOOK_SECRET!,
62
+ });
63
+ if (event.type === 'knowledge.document.published')
64
+ console.log(event.data.document_id);
65
+ return new Response(null, { status: 204 });
66
+ } catch (error) {
67
+ if (error instanceof WebhookVerificationError)
68
+ return new Response('Bad signature', { status: 400 });
69
+ throw error;
70
+ }
71
+ }
72
+ ```
73
+
74
+ Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com/), so their libraries for other languages work too. `event.id` stays the same across retries and resends; use it to drop duplicates. Payloads carry ids, statuses and counts only; read anything more through the API. `webhookEventCatalog` lists every event type with its group and the fields its `data` may carry. While a secret is rolled, pass both secrets as an array.
75
+
48
76
  ## Management commands
49
77
 
50
78
  ```ts
51
- const saved = await shuffl.units.save({
52
- id: crypto.randomUUID(), kind: 'team', name: 'Mentors',
53
- expectedRevision: 0, requestKey: 'create-mentors-2026-09',
79
+ const saved = await shuffl.groups.save({
80
+ kind: 'list', name: 'Mentors', requestKey: 'create-mentors-2026-09',
54
81
  });
55
82
  const receipt = await shuffl.commands.get({ id: saved.receipt.id });
56
- const teams = await shuffl.units.list({}); // Same persisted teams the web app reads.
83
+ const groups = await shuffl.groups.list({}); // Same groups the web app reads.
57
84
  ```
58
85
 
59
86
  Management writes return `{receipt, result}`. A duplicate request returns the same receipt with `result: null`; it never replays an action or a private result. Reusing a key with changed input fails. Read the resource after replay and reconcile an `unknown` receipt before issuing a new command. Sensitive actions require `confirm` equal to the method name (for example `knowledge.sources.publish`). Initial program/run methods retain the shapes described above.
60
87
 
61
88
  Credentials with `credentials:write` can delegate only their own scopes, with bounded expiry and inherited revocation. Account sign-in, switching to another team, provider consent and payment confirmation remain explicit browser/account flows.
62
89
 
90
+ ## Company settings
91
+
92
+ A company key (`shuffl_company_`), created by a company owner or administrator in **Company settings → API keys**, manages one company's settings and never reaches a workspace. `createShufflCompany` takes it and calls the REST operations under `/api/v1/company`:
93
+
94
+ ```ts
95
+ import { createShufflCompany } from '@shuffl/sdk';
96
+
97
+ const company = createShufflCompany({
98
+ baseUrl: process.env.SHUFFL_BASE_URL!,
99
+ token: process.env.SHUFFL_COMPANY_KEY!,
100
+ });
101
+
102
+ const { permissions } = await company.current(); // Any company key.
103
+ const page = await company.members.list({ limit: 100 });
104
+ await company.members.remove({ userId: page.items[0]!.userId });
105
+ ```
106
+
107
+ `company.current()` returns the key's company and grants. Every other method needs its grant: `company.details`, `company.workspaces.list`, `company.members.list`, `company.emailDomains.list`, `company.slack.list`, `company.rename`, `company.workspaces.setDefault` and `company.members.remove`. `members.list` pages with `limit` and `cursor` like `users.list`. Company writes run once when called and take no `requestKey`. A company key can't call `createShuffl` methods, and a workspace key can't call these. See [company keys](https://shuffl-rebuild.vercel.app/docs/api-permissions#company-keys).
108
+
63
109
  Full guide: [Use the SDK and HTTP API](https://shuffl-rebuild.vercel.app/docs/sdk).
package/dist/index.d.ts CHANGED
@@ -1,12 +1,24 @@
1
- import type { ContractRouterClient } from '@orpc/contract';
1
+ import { type ContractRouterClient } from '@orpc/contract';
2
2
  import type { ShufflContract } from '@shuffl/api-contracts';
3
+ import { type ShufflCompanyContract } from '@shuffl/api-contracts/company';
3
4
  export { ORPCError } from '@orpc/client';
4
- export type { Permission, PermissionDefinition, Scope, } from '@shuffl/api-contracts';
5
+ export type { CompanyPermission, Permission, PermissionDefinition, Scope, } from '@shuffl/api-contracts';
6
+ export { isWebhookEventType, signWebhook, verifyWebhook, type WebhookActor, type WebhookEvent, type WebhookEventData, type WebhookEventGroup, type WebhookEventType, WebhookVerificationError, webhookApiVersion, webhookEventCatalog, webhookEventGroups, webhookEventTypes, } from '@shuffl/api-contracts/webhooks';
5
7
  export type ShufflClient = ContractRouterClient<ShufflContract>;
8
+ /** The company API (#1692), for a company key (shuffl_company_). */
9
+ export type ShufflCompanyClient = ContractRouterClient<ShufflCompanyContract>;
6
10
  export interface ShufflOptions {
7
11
  token: string | (() => string | Promise<string>);
8
12
  /** Shuffl application origin, without /api/v1/rpc. */
9
13
  baseUrl: string;
10
14
  fetch?: typeof globalThis.fetch;
11
15
  }
16
+ /** A client for workspace operations, with a workspace key (shuffl_api_). */
12
17
  export declare function createShuffl(options: ShufflOptions): ShufflClient;
18
+ /**
19
+ * A client for the company API, with a company key (shuffl_company_) from
20
+ * Company settings → API keys. It calls the REST operations under
21
+ * /api/v1/company: `company.members.list({ limit: 100 })`. A company key
22
+ * never reaches workspace operations, and a workspace key never reaches these.
23
+ */
24
+ export declare function createShufflCompany(options: ShufflOptions): ShufflCompanyClient;
package/dist/index.js CHANGED
@@ -1,23 +1,97 @@
1
- import { createORPCClient } from '@orpc/client';
1
+ import { createORPCClient, ORPCError } from '@orpc/client';
2
2
  import { RPCLink } from '@orpc/client/fetch';
3
+ import { isContractProcedure } from '@orpc/contract';
4
+ import { companyContract, } from '@shuffl/api-contracts/company';
3
5
  export { ORPCError } from '@orpc/client';
4
- export function createShuffl(options) {
5
- const base = new URL(options.baseUrl);
6
+ // #643: check and type the webhooks Shuffl sends.
7
+ export { isWebhookEventType, signWebhook, verifyWebhook, WebhookVerificationError, webhookApiVersion, webhookEventCatalog, webhookEventGroups, webhookEventTypes, } from '@shuffl/api-contracts/webhooks';
8
+ function origin(baseUrl) {
9
+ const base = new URL(baseUrl);
6
10
  if (base.username ||
7
11
  base.password ||
8
12
  (base.protocol !== 'https:' &&
9
13
  !(base.protocol === 'http:' &&
10
14
  ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname))))
11
15
  throw new Error('Use HTTPS, or HTTP on localhost for development.');
16
+ return base;
17
+ }
18
+ const bearer = async (token) => `Bearer ${typeof token === 'function' ? await token() : token}`;
19
+ /** A client for workspace operations, with a workspace key (shuffl_api_). */
20
+ export function createShuffl(options) {
21
+ const base = origin(options.baseUrl);
12
22
  return createORPCClient(new RPCLink({
13
23
  // Writes are not automatically retried. Callers own stable request keys.
14
24
  fetch: async (request, init) => (options.fetch ?? globalThis.fetch)(request, {
15
25
  ...init,
16
26
  redirect: 'error',
17
27
  }),
18
- headers: async () => ({
19
- authorization: `Bearer ${typeof options.token === 'function' ? await options.token() : options.token}`,
20
- }),
28
+ headers: async () => ({ authorization: await bearer(options.token) }),
21
29
  url: new URL('/api/v1/rpc', base).toString(),
22
30
  }));
23
31
  }
32
+ const statusCodes = {
33
+ 400: 'BAD_REQUEST',
34
+ 401: 'UNAUTHORIZED',
35
+ 403: 'FORBIDDEN',
36
+ 404: 'NOT_FOUND',
37
+ 405: 'METHOD_NOT_SUPPORTED',
38
+ 409: 'CONFLICT',
39
+ 429: 'TOO_MANY_REQUESTS',
40
+ };
41
+ /**
42
+ * A client for the company API, with a company key (shuffl_company_) from
43
+ * Company settings → API keys. It calls the REST operations under
44
+ * /api/v1/company: `company.members.list({ limit: 100 })`. A company key
45
+ * never reaches workspace operations, and a workspace key never reaches these.
46
+ */
47
+ export function createShufflCompany(options) {
48
+ const base = origin(options.baseUrl);
49
+ const procedure = ({ method, path }) => async (input, call) => {
50
+ const url = new URL(`/api/v1${path}`, base);
51
+ const read = method === 'GET';
52
+ if (read)
53
+ for (const [key, value] of Object.entries(input ?? {}))
54
+ if (value !== undefined)
55
+ url.searchParams.set(key, String(value));
56
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
57
+ body: read ? undefined : JSON.stringify(input ?? {}),
58
+ headers: {
59
+ accept: 'application/json',
60
+ authorization: await bearer(options.token),
61
+ ...(read ? {} : { 'content-type': 'application/json' }),
62
+ },
63
+ method,
64
+ redirect: 'error',
65
+ signal: call?.signal,
66
+ });
67
+ const text = await response.text();
68
+ let body;
69
+ try {
70
+ body = text ? JSON.parse(text) : null;
71
+ }
72
+ catch {
73
+ body = undefined;
74
+ }
75
+ if (!response.ok) {
76
+ const error = (body ?? {});
77
+ throw new ORPCError(typeof error.code === 'string'
78
+ ? error.code
79
+ : (statusCodes[response.status] ?? 'INTERNAL_SERVER_ERROR'), {
80
+ message: typeof error.message === 'string'
81
+ ? error.message
82
+ : `Shuffl answered ${response.status}.`,
83
+ status: response.status,
84
+ });
85
+ }
86
+ if (body === undefined)
87
+ throw new ORPCError('INTERNAL_SERVER_ERROR', {
88
+ message: 'Shuffl returned a response that is not JSON.',
89
+ status: response.status,
90
+ });
91
+ return body;
92
+ };
93
+ const build = (node) => isContractProcedure(node)
94
+ ? procedure(node['~orpc'].route)
95
+ : Object.fromEntries(Object.entries(node).map(([key, value]) => [key, build(value)]));
96
+ return build(companyContract);
97
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dependencies": {
3
3
  "@orpc/client": "1.15.3",
4
4
  "@orpc/contract": "1.15.3",
5
- "@shuffl/api-contracts": "0.3.0"
5
+ "@shuffl/api-contracts": "0.5.0"
6
6
  },
7
7
  "description": "TypeScript client for the Shuffl API: people, teams, knowledge, programs and management commands.",
8
8
  "engines": {
@@ -34,5 +34,5 @@
34
34
  "sideEffects": false,
35
35
  "type": "module",
36
36
  "types": "./dist/index.d.ts",
37
- "version": "0.3.0"
37
+ "version": "0.5.0"
38
38
  }