@forumulate/sdk 0.7.0 → 0.10.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 ADDED
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ Releases of `@forumulate/sdk`. Earlier versions are described in the git history of
4
+ `packages/sdk` (`release(sdk): …` commits).
5
+
6
+ ## 0.10.0 (unreleased)
7
+
8
+ Cache, trash and destroy under their own names; publish runs; the token, named.
9
+
10
+ ### Added
11
+
12
+ - `client.cache.purge({ orgId, space?, page? })` and the generated `cache.purgePage`,
13
+ `cache.purgeSpace`, `cache.purgeOrg` (`Cache_*`): drop cached copies — our web tier and
14
+ every CDN the org configured — and answer which addresses went and which `edge` took them
15
+ (`none` when no CDN fronts them). Nothing is deleted.
16
+ - `cache.providers.*` (`EdgeProvider_*`): configure Fastly, Bunny, KeyCDN or a signed purge
17
+ webhook beside Cloudflare.
18
+ - `spaces.destroy()` / `pages.destroy()` (`Space_destroy`, `Page_destroy`): delete a TRASHED
19
+ space or page for good, with `confirm: '<slug>'`. `spaces.purge()` / `pages.purge()` remain
20
+ as deprecated names; from `Forumulate-Version: 2027-01-01` they require `confirm` too.
21
+ - `pages.publishRuns.*` (`PublishRun_*`): report, list and read docs-as-code runs, and
22
+ `syncState` — which pages were edited since the last sync, by whom and when.
23
+ - `pages.publish()` takes `purge: 'page' | 'space' | 'none'` and `syncBaseline`, and answers
24
+ `purged`. `auth.me()` names the token: `id`, `name`, `last4`, `expiresAt`, `capabilities`,
25
+ `constraints`.
26
+ - Webhook event `publish.completed`.
27
+
28
+ ### Deprecated
29
+
30
+ - `privacy.consent.purgeSpace()` / `purgeOrg()`: use `cache.purge()`.
31
+
32
+ ## 0.9.0
33
+
34
+ The policy acceptance gate, from your server.
35
+
36
+ ### Added
37
+
38
+ - `client.policies`: `status`, `accept`, `claim`, `ticket`, `verify`, `acceptances`, `export`,
39
+ `erase`, `list` (Forumulate policy feed v1, signed with a link secret or public) and `release`
40
+ (one exact published version).
41
+ - `verifyAcceptanceToken(token, { keys | secret | jwks | jwksUrl, subject?, spaceId?, orgId?,
42
+ requireSatisfied? })`: offline verification of the gate's acceptance tokens and receipts
43
+ (HS256 JWT, `kid`-selected keyring, so a key rotation never breaks a token in flight). Refuses a
44
+ ticket presented as an acceptance (`wrong_use`), `alg` other than HS256, and expired tokens.
45
+ - `signPolicyTicket`, `policyGatePageUrl` and `readPolicyReturn`: the hosted acceptance page round
46
+ trip with no API call. `verifyPolicyTicket` checks a ticket of yours that comes back to your
47
+ server (a bridge between an auth platform's redirect and the hosted page). `parsePolicyGateKeys` reads `FORUMULATE_POLICY_GATE_KEYS`;
48
+ `policyEmailHash` and `policySha256Hex` are the gate's two hashes.
49
+ - `acceptanceItemsFrom(status, { presentation })`: the items to record after your own UI showed
50
+ the policies.
51
+ - Forumulate policy feed v1 protocol: `signPolicyFeedRequest` / `verifyPolicyFeedRequest`
52
+ (`forumulate-*` or `atlas-*` headers, millisecond timestamps, rotation, replay hook) and
53
+ `verifyPolicyWebhook` / `signPolicyWebhook` (Standard Webhooks, `whsec_` secrets) for
54
+ `policy.published` and `policy.retired`.
55
+
56
+ Not published to npm yet: publishing is the integrator's release step.
package/README.md CHANGED
@@ -192,6 +192,45 @@ const es = await client.translations.get(orgId, spaceId, pageId, 'es'); // { tit
192
192
  await client.translations.put(orgId, spaceId, pageId, 'es', { markdown: '…', published: true });
193
193
  ```
194
194
 
195
+ ### 7. Gate sign-up and sign-in on your published policies
196
+
197
+ `client.policies` and three standalone functions are the server half of the
198
+ [policy acceptance gate](https://forumulate.com/guides/policy-acceptance-gate): your Legal space's
199
+ published Terms, Privacy Policy and so on, accepted by your users in any app, whatever signs them in.
200
+
201
+ ```ts
202
+ import { ForumulateClient, signPolicyTicket, verifyAcceptanceToken, policyGatePageUrl } from '@forumulate/sdk';
203
+
204
+ const client = ForumulateClient.fromEnv();
205
+ const space = { orgId: process.env.FORUMULATE_ORG!, spaceId: process.env.FORUMULATE_LEGAL_SPACE! };
206
+
207
+ // What does this user still have to accept before signing in?
208
+ const status = await client.policies.status({ ...space, subject: user.id, event: 'sign_in' });
209
+
210
+ if (!status.satisfied) {
211
+ // Send them to the hosted acceptance page with a ticket you sign yourself (no API call).
212
+ const ticket = await signPolicyTicket(
213
+ { ...space, subject: user.id, event: 'sign_in', returnUrl: 'https://app.example.com/policy/return' },
214
+ process.env.FORUMULATE_POLICY_GATE_KEYS!, // "pgk_…:pgs_…"
215
+ );
216
+ return redirect(policyGatePageUrl({ ...space, ticket }));
217
+ }
218
+
219
+ // On the way back (or from the widget): verify the acceptance token offline, with key rotation.
220
+ const r = await verifyAcceptanceToken(token, {
221
+ keys: process.env.FORUMULATE_POLICY_GATE_KEYS,
222
+ subject: user.id,
223
+ requireSatisfied: true,
224
+ });
225
+ ```
226
+
227
+ Also `client.policies.accept` / `claim` / `verify` / `acceptances` / `export` / `erase`, and
228
+ `client.policies.list({ spaceId, secret })` to read Forumulate policy feed v1.
229
+ `verifyPolicyWebhook(headers, rawBody, secret)` checks a `policy.published` / `policy.retired`
230
+ notice, and `signPolicyFeedRequest` / `verifyPolicyFeedRequest` implement the feed's request
231
+ signing, so any platform can consume the feed or serve its own. Recipes for Auth0, Clerk, Supabase,
232
+ Firebase, Auth.js and a custom backend: [Policy gate recipes](https://forumulate.com/guides/policy-gate-recipes).
233
+
195
234
  ## Every endpoint: `typed()`
196
235
 
197
236
  The helpers above cover the common paths. `typed(method, path, init)` covers every route in the OpenAPI spec, with the path, params, query, body and response all inferred, and autocomplete on path and method:
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `client.cache.purge()` — drop cached copies, delete nothing (FMU-B04).
3
+ *
4
+ * The generated `cache.purgeSpace / purgePage / purgeOrg` are the three
5
+ * operations; this is the one call a deploy script wants: name the space (and
6
+ * a page, or neither for the whole org) and get back which addresses went and
7
+ * which CDN took them. The old `privacy.consent.purgeSpace()` still works and
8
+ * runs the same purge. Destroying a trashed page or space is
9
+ * `pages.destroy()` / `spaces.destroy()` — a different word on purpose.
10
+ */
11
+ export interface CachePurgeResult {
12
+ /** `page`, `space` or `org`. */
13
+ purged: string;
14
+ urls: string[];
15
+ urlCount: number;
16
+ /** The CDN that took it (`cloudflare`, `fastly`, …), `multiple`, or `none` when none fronts it. */
17
+ edge: string;
18
+ edges: {
19
+ name: string;
20
+ account: 'platform' | 'org';
21
+ urls: number;
22
+ ok: boolean;
23
+ error?: string;
24
+ }[];
25
+ /** Our own web tier was refreshed. */
26
+ webTier: boolean;
27
+ }
28
+ export interface CachePurgeArgs {
29
+ orgId: string;
30
+ /** Space id (or slug where the API accepts one). Omit for the whole organization. */
31
+ space?: string;
32
+ /** Page id within `space`: just that page. */
33
+ page?: string;
34
+ }
35
+ type Http = <T>(method: string, path: string) => Promise<T>;
36
+ export declare function cacheApi(http: Http): {
37
+ /** `Cache_purgePage`: one page's every address. */
38
+ purgePage(args: {
39
+ orgId: string;
40
+ spaceId: string;
41
+ pageId: string;
42
+ }): Promise<CachePurgeResult>;
43
+ /** `Cache_purgeSpace`: everything under the space. */
44
+ purgeSpace(args: {
45
+ orgId: string;
46
+ spaceId: string;
47
+ }): Promise<CachePurgeResult>;
48
+ /** `Cache_purgeOrg`: every space in the organization. */
49
+ purgeOrg(args: {
50
+ orgId: string;
51
+ }): Promise<CachePurgeResult>;
52
+ /** Purge one page, one space, or (no `space`) every space in the org. */
53
+ purge(args: CachePurgeArgs): Promise<CachePurgeResult>;
54
+ };
55
+ export {};
56
+ //# sourceMappingURL=cache.d.ts.map
package/dist/cache.js ADDED
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cacheApi = cacheApi;
4
+ function cacheApi(http) {
5
+ const space = (orgId, spaceId) => `/orgs/${encodeURIComponent(orgId)}/spaces/${encodeURIComponent(spaceId)}`;
6
+ return {
7
+ /** `Cache_purgePage`: one page's every address. */
8
+ purgePage(args) {
9
+ return http('POST', `${space(args.orgId, args.spaceId)}/pages/${encodeURIComponent(args.pageId)}/cache/purge`);
10
+ },
11
+ /** `Cache_purgeSpace`: everything under the space. */
12
+ purgeSpace(args) {
13
+ return http('POST', `${space(args.orgId, args.spaceId)}/cache/purge`);
14
+ },
15
+ /** `Cache_purgeOrg`: every space in the organization. */
16
+ purgeOrg(args) {
17
+ return http('POST', `/orgs/${encodeURIComponent(args.orgId)}/cache/purge`);
18
+ },
19
+ /** Purge one page, one space, or (no `space`) every space in the org. */
20
+ purge(args) {
21
+ const org = encodeURIComponent(args.orgId);
22
+ if (!args.space) {
23
+ if (args.page)
24
+ throw new Error('cache.purge: `page` needs `space`.');
25
+ return http('POST', `/orgs/${org}/cache/purge`);
26
+ }
27
+ const space = `/orgs/${org}/spaces/${encodeURIComponent(args.space)}`;
28
+ return http('POST', args.page
29
+ ? `${space}/pages/${encodeURIComponent(args.page)}/cache/purge`
30
+ : `${space}/cache/purge`);
31
+ },
32
+ };
33
+ }
34
+ //# sourceMappingURL=cache.js.map
package/dist/http.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * and not another.
7
7
  */
8
8
  /** Kept in step with package.json by `coverage.test.ts`. */
9
- export declare const SDK_VERSION = "0.7.0";
9
+ export declare const SDK_VERSION = "0.10.0";
10
10
  /** The API's error envelope: `{ error: { code, message, details? }, requestId? }`. */
11
11
  export interface ForumulateErrorInit {
12
12
  /** Field-level detail, e.g. `{ title: ['Required'] }` for `validation.failed`. */
package/dist/http.js CHANGED
@@ -11,7 +11,7 @@ exports.ForumulateError = exports.SDK_VERSION = void 0;
11
11
  exports.retryDelayMs = retryDelayMs;
12
12
  exports.send = send;
13
13
  /** Kept in step with package.json by `coverage.test.ts`. */
14
- exports.SDK_VERSION = '0.7.0';
14
+ exports.SDK_VERSION = '0.10.0';
15
15
  /**
16
16
  * Thrown for any non-2xx API response, and for a network failure that
17
17
  * outlived its retries (`status: 0`, `code: 'network.error'`). Mirrors the
package/dist/index.d.ts CHANGED
@@ -2,6 +2,11 @@ export * from './types';
2
2
  export { verifyWebhookSignature, constructWebhookEvent, type VerifyWebhookInput } from './webhooks';
3
3
  export * from './content';
4
4
  export * from './translations';
5
+ export * from './policies';
6
+ export * from './cache';
7
+ export * from './policy-tokens';
8
+ export * from './policy-tickets';
9
+ export * from './policy-feed';
5
10
  export type { paths, operations } from './schema';
6
11
  export type { HttpMethod, MethodsOf, OperationOf, TypedRequestInit, TypedResponse } from './typed';
7
12
  export { ForumulateError, SDK_VERSION, retryDelayMs, type CallOptions, type ForumulateErrorInit, } from './http';
@@ -10,6 +15,7 @@ export type { Namespace, NamespaceName, OperationArgs, OperationFn, OperationId,
10
15
  export { API_VERSION } from './operations.gen';
11
16
  import type { paths } from './schema';
12
17
  import type { HttpMethod, TypedRequestInit, TypedResponse } from './typed';
18
+ import { cacheApi } from './cache';
13
19
  import { type Query } from './http';
14
20
  import { type PositionalHelpers } from './helpers';
15
21
  import { type Merged, type Namespace, type OperationArgs, type OptionalArgsOperationId, type OperationId, type OperationReturn, type OperationRow, type PagedOperationId, type Resources } from './resources';
@@ -62,7 +68,7 @@ export interface ForumulateClientOptions {
62
68
  */
63
69
  userAgent?: string;
64
70
  }
65
- type HandWritten = keyof PositionalHelpers | 'content';
71
+ type HandWritten = keyof PositionalHelpers | 'content' | 'policies' | 'cache';
66
72
  /**
67
73
  * Typed client for the Forumulate REST API.
68
74
  *
@@ -488,6 +494,81 @@ export declare class ForumulateClient {
488
494
  }>;
489
495
  };
490
496
  };
497
+ /** The policy acceptance gate: status, accept, claim, verify, the feed — see ./policies. */
498
+ readonly policies: {
499
+ status: (input: import("./policies").PolicyStatusInput) => Promise<import("./policies").PolicyGateStatus>;
500
+ accept: (input: import("./policies").PolicyAcceptInput) => Promise<import("./policies").PolicyAcceptanceResult>;
501
+ claim: (input: import("./policies").PolicyGateSpace & import("./policies").PolicySubjectIdentity & {
502
+ receipt: string;
503
+ subject: string;
504
+ }) => Promise<import("./policies").PolicyAcceptanceResult>;
505
+ ticket: (input: import("./policies").PolicyGateSpace & import("./policies").PolicySubjectIdentity & {
506
+ subject: string;
507
+ event: import("./policy-tokens").PolicyGateEvent;
508
+ sourceApp?: string;
509
+ returnUrl?: string;
510
+ state?: string;
511
+ locale?: string;
512
+ ttlSeconds?: number;
513
+ }) => Promise<{
514
+ ticket: string;
515
+ expiresAt: string;
516
+ pageUrl: string;
517
+ }>;
518
+ verify: (input: import("./policies").PolicyGateSpace & {
519
+ token: string;
520
+ subject?: string;
521
+ }) => Promise<{
522
+ valid: boolean;
523
+ reason?: string;
524
+ claims?: Record<string, unknown>;
525
+ }>;
526
+ acceptances: (input: import("./policies").PolicyGateSpace & import("./policies").PolicySubjectIdentity & {
527
+ subject?: string;
528
+ key?: string;
529
+ cursor?: string;
530
+ limit?: number;
531
+ }) => Promise<{
532
+ acceptances: import("./policies").PolicyAcceptanceRow[];
533
+ page: {
534
+ limit: number;
535
+ hasMore: boolean;
536
+ nextCursor: string | null;
537
+ };
538
+ }>;
539
+ export: (input: import("./policies").PolicyGateSpace & {
540
+ format: "csv" | "json";
541
+ subject?: string;
542
+ emailHash?: string;
543
+ key?: string;
544
+ }) => Promise<string>;
545
+ erase: (input: {
546
+ orgId: string;
547
+ } & import("./policies").PolicySubjectIdentity & {
548
+ subject?: string;
549
+ }) => Promise<{
550
+ erased: number;
551
+ }>;
552
+ list: (input: import("./policies").PolicyListInput) => Promise<import("./policy-feed").PolicyFeedList>;
553
+ release: (input: {
554
+ orgId: string;
555
+ key: string;
556
+ version: string;
557
+ }) => Promise<{
558
+ release: {
559
+ kind: string;
560
+ version: string;
561
+ title: string;
562
+ contentHtml: string;
563
+ contentSha256: string;
564
+ publishedAt: string;
565
+ effectiveAt: string;
566
+ changeNote: string | null;
567
+ };
568
+ }>;
569
+ };
570
+ /** `cache.purge({ orgId, space, page })` over the generated `Cache_*` methods (FMU-B04). */
571
+ readonly cache: ReturnType<typeof cacheApi>;
491
572
  readonly orgs: Merged<Namespace<"orgs">, {
492
573
  list: () => Promise<{
493
574
  orgs: import("./types").Organization[];
package/dist/index.js CHANGED
@@ -21,6 +21,11 @@ Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get
21
21
  Object.defineProperty(exports, "constructWebhookEvent", { enumerable: true, get: function () { return webhooks_1.constructWebhookEvent; } });
22
22
  __exportStar(require("./content"), exports);
23
23
  __exportStar(require("./translations"), exports);
24
+ __exportStar(require("./policies"), exports);
25
+ __exportStar(require("./cache"), exports);
26
+ __exportStar(require("./policy-tokens"), exports);
27
+ __exportStar(require("./policy-tickets"), exports);
28
+ __exportStar(require("./policy-feed"), exports);
24
29
  var http_1 = require("./http");
25
30
  Object.defineProperty(exports, "ForumulateError", { enumerable: true, get: function () { return http_1.ForumulateError; } });
26
31
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return http_1.SDK_VERSION; } });
@@ -31,6 +36,8 @@ var operations_gen_1 = require("./operations.gen");
31
36
  Object.defineProperty(exports, "API_VERSION", { enumerable: true, get: function () { return operations_gen_1.API_VERSION; } });
32
37
  const content_1 = require("./content");
33
38
  const translations_1 = require("./translations");
39
+ const policies_1 = require("./policies");
40
+ const cache_1 = require("./cache");
34
41
  const http_2 = require("./http");
35
42
  const pagination_2 = require("./pagination");
36
43
  const helpers_1 = require("./helpers");
@@ -182,6 +189,13 @@ class ForumulateClient {
182
189
  content = this.ns('content', (0, content_1.contentApi)((method, path, opts) => this.http(method, path, opts)));
183
190
  /** Translations of every kind of piece, routed by space type — see ./translations. */
184
191
  translations = (0, translations_1.translationsApi)((method, path, opts) => this.http(method, path, opts));
192
+ /** The policy acceptance gate: status, accept, claim, verify, the feed — see ./policies. */
193
+ policies = (0, policies_1.policiesApi)((method, path, opts = {}) => (0, http_2.send)(this.transport, { method, path, ...opts, version: this.apiVersion }), () => this.transport.baseUrl);
194
+ /** `cache.purge({ orgId, space, page })` over the generated `Cache_*` methods (FMU-B04). */
195
+ // Merged by hand rather than through `ns`: the hand-written half names every
196
+ // Cache_* operation itself, so the client type does not depend on the
197
+ // generated table having been refreshed first (the build runs before it).
198
+ cache = (0, resources_1.mergeNamespace)((0, resources_1.buildNamespace)('cache', this.invoke), (0, cache_1.cacheApi)((method, path) => this.http(method, path)));
185
199
  orgs = this.ns('orgs', this.helpers.orgs);
186
200
  spaces = this.ns('spaces', this.helpers.spaces);
187
201
  shapes = this.ns('shapes', this.helpers.shapes);