@brass-build/client 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/AGENTS.md +255 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +836 -0
- package/dist/api-fetch.d.ts +16 -0
- package/dist/api-fetch.d.ts.map +1 -0
- package/dist/api-fetch.js +97 -0
- package/dist/api-fetch.js.map +1 -0
- package/dist/api-headers.d.ts +3 -0
- package/dist/api-headers.d.ts.map +1 -0
- package/dist/api-headers.js +25 -0
- package/dist/api-headers.js.map +1 -0
- package/dist/auth-flow.d.ts +23 -0
- package/dist/auth-flow.d.ts.map +1 -0
- package/dist/auth-flow.js +76 -0
- package/dist/auth-flow.js.map +1 -0
- package/dist/client.d.ts +106 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1278 -0
- package/dist/client.js.map +1 -0
- package/dist/doc-url-sync.d.ts +19 -0
- package/dist/doc-url-sync.d.ts.map +1 -0
- package/dist/doc-url-sync.js +181 -0
- package/dist/doc-url-sync.js.map +1 -0
- package/dist/document-branches.d.ts +32 -0
- package/dist/document-branches.d.ts.map +1 -0
- package/dist/document-branches.js +89 -0
- package/dist/document-branches.js.map +1 -0
- package/dist/document-checkpoints.d.ts +24 -0
- package/dist/document-checkpoints.d.ts.map +1 -0
- package/dist/document-checkpoints.js +115 -0
- package/dist/document-checkpoints.js.map +1 -0
- package/dist/document-commands.d.ts +93 -0
- package/dist/document-commands.d.ts.map +1 -0
- package/dist/document-commands.js +423 -0
- package/dist/document-commands.js.map +1 -0
- package/dist/document-signals.d.ts +53 -0
- package/dist/document-signals.d.ts.map +1 -0
- package/dist/document-signals.js +114 -0
- package/dist/document-signals.js.map +1 -0
- package/dist/document-streams.d.ts +22 -0
- package/dist/document-streams.d.ts.map +1 -0
- package/dist/document-streams.js +98 -0
- package/dist/document-streams.js.map +1 -0
- package/dist/document-thumbnail-capture.d.ts +27 -0
- package/dist/document-thumbnail-capture.d.ts.map +1 -0
- package/dist/document-thumbnail-capture.js +160 -0
- package/dist/document-thumbnail-capture.js.map +1 -0
- package/dist/documents-namespace.d.ts +134 -0
- package/dist/documents-namespace.d.ts.map +1 -0
- package/dist/documents-namespace.js +190 -0
- package/dist/documents-namespace.js.map +1 -0
- package/dist/documents.d.ts +146 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +2045 -0
- package/dist/documents.js.map +1 -0
- package/dist/emitter.d.ts +15 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +90 -0
- package/dist/emitter.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/export-guest.d.ts +9 -0
- package/dist/export-guest.d.ts.map +1 -0
- package/dist/export-guest.js +225 -0
- package/dist/export-guest.js.map +1 -0
- package/dist/fetch-retry.d.ts +16 -0
- package/dist/fetch-retry.d.ts.map +1 -0
- package/dist/fetch-retry.js +90 -0
- package/dist/fetch-retry.js.map +1 -0
- package/dist/guest-link.d.ts +3 -0
- package/dist/guest-link.d.ts.map +1 -0
- package/dist/guest-link.js +13 -0
- package/dist/guest-link.js.map +1 -0
- package/dist/handles.d.ts +57 -0
- package/dist/handles.d.ts.map +1 -0
- package/dist/handles.js +1375 -0
- package/dist/handles.js.map +1 -0
- package/dist/hosted-runtime.d.ts +33 -0
- package/dist/hosted-runtime.d.ts.map +1 -0
- package/dist/hosted-runtime.js +99 -0
- package/dist/hosted-runtime.js.map +1 -0
- package/dist/hosted-ui.d.ts +84 -0
- package/dist/hosted-ui.d.ts.map +1 -0
- package/dist/hosted-ui.js +673 -0
- package/dist/hosted-ui.js.map +1 -0
- package/dist/importer.d.ts +22 -0
- package/dist/importer.d.ts.map +1 -0
- package/dist/importer.js +54 -0
- package/dist/importer.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/init-keys.d.ts +3 -0
- package/dist/init-keys.d.ts.map +1 -0
- package/dist/init-keys.js +13 -0
- package/dist/init-keys.js.map +1 -0
- package/dist/internal.d.ts +6 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +17 -0
- package/dist/internal.js.map +1 -0
- package/dist/jwt.d.ts +3 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +22 -0
- package/dist/jwt.js.map +1 -0
- package/dist/launch-context.d.ts +4 -0
- package/dist/launch-context.d.ts.map +1 -0
- package/dist/launch-context.js +44 -0
- package/dist/launch-context.js.map +1 -0
- package/dist/login-hint.d.ts +4 -0
- package/dist/login-hint.d.ts.map +1 -0
- package/dist/login-hint.js +63 -0
- package/dist/login-hint.js.map +1 -0
- package/dist/mutate-batching.d.ts +14 -0
- package/dist/mutate-batching.d.ts.map +1 -0
- package/dist/mutate-batching.js +65 -0
- package/dist/mutate-batching.js.map +1 -0
- package/dist/open-document.d.ts +127 -0
- package/dist/open-document.d.ts.map +1 -0
- package/dist/open-document.js +2121 -0
- package/dist/open-document.js.map +1 -0
- package/dist/page-title.d.ts +3 -0
- package/dist/page-title.d.ts.map +1 -0
- package/dist/page-title.js +32 -0
- package/dist/page-title.js.map +1 -0
- package/dist/perf.d.ts +9 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +39 -0
- package/dist/perf.js.map +1 -0
- package/dist/presence.d.ts +62 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +331 -0
- package/dist/presence.js.map +1 -0
- package/dist/record-paging.d.ts +99 -0
- package/dist/record-paging.d.ts.map +1 -0
- package/dist/record-paging.js +220 -0
- package/dist/record-paging.js.map +1 -0
- package/dist/schema-conflict-dialog.d.ts +10 -0
- package/dist/schema-conflict-dialog.d.ts.map +1 -0
- package/dist/schema-conflict-dialog.js +164 -0
- package/dist/schema-conflict-dialog.js.map +1 -0
- package/dist/selection-persistence.d.ts +5 -0
- package/dist/selection-persistence.d.ts.map +1 -0
- package/dist/selection-persistence.js +90 -0
- package/dist/selection-persistence.js.map +1 -0
- package/dist/serializer.d.ts +3 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +2 -0
- package/dist/serializer.js.map +1 -0
- package/dist/session-persistence.d.ts +4 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +68 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/session-toolbar.d.ts +62 -0
- package/dist/session-toolbar.d.ts.map +1 -0
- package/dist/session-toolbar.js +1205 -0
- package/dist/session-toolbar.js.map +1 -0
- package/dist/session.d.ts +69 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +926 -0
- package/dist/session.js.map +1 -0
- package/dist/tail-ops.d.ts +28 -0
- package/dist/tail-ops.d.ts.map +1 -0
- package/dist/tail-ops.js +46 -0
- package/dist/tail-ops.js.map +1 -0
- package/dist/topic-lifecycle.d.ts +41 -0
- package/dist/topic-lifecycle.d.ts.map +1 -0
- package/dist/topic-lifecycle.js +169 -0
- package/dist/topic-lifecycle.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/undo.d.ts +18 -0
- package/dist/undo.d.ts.map +1 -0
- package/dist/undo.js +110 -0
- package/dist/undo.js.map +1 -0
- package/dist/version-bar.d.ts +22 -0
- package/dist/version-bar.d.ts.map +1 -0
- package/dist/version-bar.js +169 -0
- package/dist/version-bar.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/dist/vite.d.ts +15 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +36 -0
- package/dist/vite.js.map +1 -0
- package/dist/ws-client.d.ts +53 -0
- package/dist/ws-client.d.ts.map +1 -0
- package/dist/ws-client.js +604 -0
- package/dist/ws-client.js.map +1 -0
- package/dist/ws-frame-limit.d.ts +21 -0
- package/dist/ws-frame-limit.d.ts.map +1 -0
- package/dist/ws-frame-limit.js +40 -0
- package/dist/ws-frame-limit.js.map +1 -0
- package/docs/app-manifest.md +65 -0
- package/docs/custom-chrome.md +147 -0
- package/docs/hosted-ui-advanced.md +77 -0
- package/docs/imported-documents.md +261 -0
- package/docs/presence.md +129 -0
- package/docs/schemas.md +135 -0
- package/docs/streams.md +248 -0
- package/package.json +60 -0
- package/scripts/prepare.mjs +50 -0
- package/src/api-fetch.ts +127 -0
- package/src/api-headers.ts +28 -0
- package/src/auth-flow.ts +125 -0
- package/src/client.ts +1457 -0
- package/src/doc-url-sync.ts +183 -0
- package/src/document-branches.ts +124 -0
- package/src/document-checkpoints.ts +135 -0
- package/src/document-commands.ts +651 -0
- package/src/document-signals.ts +234 -0
- package/src/document-streams.ts +132 -0
- package/src/document-thumbnail-capture.ts +191 -0
- package/src/documents-namespace.ts +516 -0
- package/src/documents.ts +2426 -0
- package/src/emitter.ts +129 -0
- package/src/errors.ts +45 -0
- package/src/export-guest.ts +282 -0
- package/src/fetch-retry.ts +119 -0
- package/src/guest-link.ts +14 -0
- package/src/handles.ts +1731 -0
- package/src/hosted-runtime.ts +109 -0
- package/src/hosted-ui.ts +1020 -0
- package/src/importer.ts +89 -0
- package/src/index.ts +267 -0
- package/src/init-keys.ts +15 -0
- package/src/internal.ts +23 -0
- package/src/jwt.ts +18 -0
- package/src/launch-context.ts +43 -0
- package/src/login-hint.ts +60 -0
- package/src/mutate-batching.ts +63 -0
- package/src/open-document.ts +2869 -0
- package/src/page-title.ts +33 -0
- package/src/perf.ts +46 -0
- package/src/presence.ts +471 -0
- package/src/record-paging.ts +270 -0
- package/src/schema-conflict-dialog.ts +176 -0
- package/src/selection-persistence.ts +98 -0
- package/src/serializer.ts +15 -0
- package/src/session-persistence.ts +69 -0
- package/src/session-toolbar.ts +1457 -0
- package/src/session.ts +1262 -0
- package/src/tail-ops.ts +63 -0
- package/src/topic-lifecycle.ts +217 -0
- package/src/types.ts +75 -0
- package/src/undo.ts +125 -0
- package/src/version-bar.ts +221 -0
- package/src/version.ts +8 -0
- package/src/vite.ts +49 -0
- package/src/ws-client.ts +678 -0
- package/src/ws-frame-limit.ts +52 -0
package/src/api-fetch.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// The shared HTTP request wrapper: resolve token → 401 if missing →
|
|
2
|
+
// auth header → app-assertion → fetch → parse → throw `BrassError` on
|
|
3
|
+
// non-2xx → return parsed. Read paths that need the raw status without
|
|
4
|
+
// throwing stay hand-written but still throw `BrassError`.
|
|
5
|
+
|
|
6
|
+
import { applyAppAssertionHeader, applyClientVersionHeader } from './api-headers.js';
|
|
7
|
+
import { BrassError } from './errors.js';
|
|
8
|
+
import { fetchWithRetry } from './fetch-retry.js';
|
|
9
|
+
|
|
10
|
+
export interface ApiRequest {
|
|
11
|
+
// Defaults to `GET`.
|
|
12
|
+
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
13
|
+
path: string;
|
|
14
|
+
// JSON-encoded when present; sets `content-type: application/json`.
|
|
15
|
+
body?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ApiFetchOptions {
|
|
19
|
+
apiBaseUrl: string;
|
|
20
|
+
getAccessToken: () => Promise<string | null>;
|
|
21
|
+
getAppAssertion?: () => Promise<string | null>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ApiFetch = <T>(req: ApiRequest) => Promise<T>;
|
|
25
|
+
|
|
26
|
+
export function createApiFetch(opts: ApiFetchOptions): ApiFetch {
|
|
27
|
+
const baseUrl = opts.apiBaseUrl.replace(/\/$/, '');
|
|
28
|
+
|
|
29
|
+
return async function apiFetch<T>(req: ApiRequest): Promise<T> {
|
|
30
|
+
const token = await opts.getAccessToken();
|
|
31
|
+
if (!token) {
|
|
32
|
+
// Reject before fetch: a deterministic 401, no network round-trip.
|
|
33
|
+
throw new BrassError('Not signed in', { status: 401 });
|
|
34
|
+
}
|
|
35
|
+
const headers: Record<string, string> = { authorization: `Bearer ${token}` };
|
|
36
|
+
if (req.body !== undefined) headers['content-type'] = 'application/json';
|
|
37
|
+
applyClientVersionHeader(headers);
|
|
38
|
+
await applyAppAssertionHeader(headers, opts.getAppAssertion);
|
|
39
|
+
|
|
40
|
+
const method = req.method ?? 'GET';
|
|
41
|
+
const init: RequestInit = { method, headers };
|
|
42
|
+
if (req.body !== undefined) init.body = JSON.stringify(req.body);
|
|
43
|
+
|
|
44
|
+
const url = `${baseUrl}${req.path}`;
|
|
45
|
+
// A GET is idempotent, so ride out a cold-API transient 5xx / 429
|
|
46
|
+
// (e.g. a 503 while the API scales into a concurrent read burst)
|
|
47
|
+
// rather than surfacing it as a hard failure; a write is never
|
|
48
|
+
// replayed here.
|
|
49
|
+
const response =
|
|
50
|
+
method === 'GET'
|
|
51
|
+
? await fetchWithRetry(() => fetch(url, init))
|
|
52
|
+
: await fetch(url, init);
|
|
53
|
+
if (response.status === 204) return undefined as T;
|
|
54
|
+
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
let parsed: unknown = undefined;
|
|
57
|
+
if (text) {
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(text);
|
|
60
|
+
} catch {
|
|
61
|
+
// Leave `parsed` undefined; a non-2xx falls back to the generic
|
|
62
|
+
// message, a 2xx surfaces as the caller's own malformed-body guard.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw brassErrorFromBody(response.status, parsed);
|
|
67
|
+
}
|
|
68
|
+
return parsed as T;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isErrorBody(body: unknown): body is { error: string } {
|
|
73
|
+
return (
|
|
74
|
+
typeof body === 'object' &&
|
|
75
|
+
body !== null &&
|
|
76
|
+
typeof (body as { error?: unknown }).error === 'string'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build the BrassError for a non-2xx response body. Every SDK fetch
|
|
81
|
+
// (apiFetch and the hand-written document open / create paths) throws
|
|
82
|
+
// through here so the server's machine-readable denials, in particular
|
|
83
|
+
// `code: "app_not_installed"`, survive into the error the caller (and
|
|
84
|
+
// the SDK's own install-consent offer) can branch on.
|
|
85
|
+
export function brassErrorFromBody(status: number, parsed: unknown): BrassError {
|
|
86
|
+
const message = isErrorBody(parsed) ? parsed.error : 'Request failed';
|
|
87
|
+
if (isNotInstalledBody(parsed)) {
|
|
88
|
+
return new BrassError(message, {
|
|
89
|
+
status,
|
|
90
|
+
code: 'app_not_installed',
|
|
91
|
+
organizationId: parsed.organization_id,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (isSchemaConflictBody(parsed)) {
|
|
95
|
+
const title = (parsed as Record<string, unknown>)['title'];
|
|
96
|
+
return new BrassError(message, {
|
|
97
|
+
status,
|
|
98
|
+
code: 'schema_conflict',
|
|
99
|
+
...(typeof title === 'string' ? { title } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return new BrassError(message, { status });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The platform refuses to join a document this app's declared schema cannot
|
|
106
|
+
// read, so a bookmark or a reload does not silently open one. The message is
|
|
107
|
+
// the server's own line, which the client's dialog renders verbatim rather
|
|
108
|
+
// than composing a reason of its own out of a shape it would have to keep in
|
|
109
|
+
// step with the platform's.
|
|
110
|
+
function isSchemaConflictBody(value: unknown): value is { error: string } {
|
|
111
|
+
return (
|
|
112
|
+
typeof value === 'object' &&
|
|
113
|
+
value !== null &&
|
|
114
|
+
(value as { code?: unknown }).code === 'schema_conflict'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isNotInstalledBody(
|
|
119
|
+
value: unknown,
|
|
120
|
+
): value is { error: string; code: 'app_not_installed'; organization_id: string } {
|
|
121
|
+
return (
|
|
122
|
+
typeof value === 'object' &&
|
|
123
|
+
value !== null &&
|
|
124
|
+
(value as { code?: unknown }).code === 'app_not_installed' &&
|
|
125
|
+
typeof (value as { organization_id?: unknown }).organization_id === 'string'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SDK_CLIENT_ID } from './version.js';
|
|
2
|
+
|
|
3
|
+
// Attaches the acting-app assertion as `X-Brass-App-Assertion` when one
|
|
4
|
+
// is available; a no-op when the caller has none.
|
|
5
|
+
//
|
|
6
|
+
// MUST only be applied to requests to the Brass data API. Never attach
|
|
7
|
+
// it to other origins, in particular the presigned snapshot/upload
|
|
8
|
+
// URLs the document path fetches, which must carry no Brass headers (a
|
|
9
|
+
// stray header would also break the presigned signature's allowed-header
|
|
10
|
+
// set).
|
|
11
|
+
export async function applyAppAssertionHeader(
|
|
12
|
+
headers: Record<string, string>,
|
|
13
|
+
getAppAssertion?: () => Promise<string | null>,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const assertion = await getAppAssertion?.();
|
|
16
|
+
if (assertion) headers['x-brass-app-assertion'] = assertion;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Attaches the SDK's version as `x-brass-client` (`sdk/<version>`), the same
|
|
20
|
+
// value reported on /refresh and the WS connect URL, so the server sees the
|
|
21
|
+
// deployed SDK version at the data-API surface too. The minimum-version gate
|
|
22
|
+
// reads it at document open to serve a too-old client an upgrade message
|
|
23
|
+
// (rendered verbatim through `session.error`). Same boundary as the
|
|
24
|
+
// app-assertion header: data API only, never a presigned URL. Unconditional, so the
|
|
25
|
+
// version distribution covers read-only apps that never refresh.
|
|
26
|
+
export function applyClientVersionHeader(headers: Record<string, string>): void {
|
|
27
|
+
headers['x-brass-client'] = SDK_CLIENT_ID;
|
|
28
|
+
}
|
package/src/auth-flow.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { SDK_CLIENT_ID } from './version.js';
|
|
2
|
+
import { fetchWithRetry, RETRY_READ, RETRY_WRITE_503 } from './fetch-retry.js';
|
|
3
|
+
|
|
4
|
+
// Fallback token lifetime if a /refresh response omits `expires_in`. The
|
|
5
|
+
// server always sends it (so this is forward-compat robustness), but without a
|
|
6
|
+
// fallback `Date.now() + undefined * 1000` is NaN, and the client's freshness
|
|
7
|
+
// check `expiresAt - leeway > Date.now()` is then always false, so every
|
|
8
|
+
// getAccessToken() would fire a fresh /refresh round-trip. Mirrors the CLI's
|
|
9
|
+
// same guard.
|
|
10
|
+
const DEFAULT_TOKEN_TTL_SECONDS = 3600;
|
|
11
|
+
|
|
12
|
+
export interface TokenSet {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
idToken: string;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
// The acting-app assertion minted at /refresh, forwarded to the data
|
|
17
|
+
// API as `X-Brass-App-Assertion`. Absent when the proxy has no signing
|
|
18
|
+
// secret configured.
|
|
19
|
+
appAssertion?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RefreshResponse {
|
|
23
|
+
access_token: string;
|
|
24
|
+
id_token: string;
|
|
25
|
+
expires_in: number;
|
|
26
|
+
app_assertion?: string;
|
|
27
|
+
// Present only for a fallback-mode request (a `brass_code` exchange or
|
|
28
|
+
// a `sid` echo): the opaque platform-session pointer the SDK holds in
|
|
29
|
+
// memory to keep refreshing without the cross-site cookie.
|
|
30
|
+
session_token?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The credential the SDK presents to POST /refresh.
|
|
34
|
+
// - `cookie`: the default; the browser attaches the `brass_auth` cookie
|
|
35
|
+
// and nothing is sent in the body. Works wherever the third-party
|
|
36
|
+
// cookie is allowed.
|
|
37
|
+
// - `code`: a one-time `brass_code` from the sign-in redirect, exchanged
|
|
38
|
+
// for tokens + a `session_token`. The incognito / Safari ITP fallback,
|
|
39
|
+
// used only after a `cookie` refresh came back empty.
|
|
40
|
+
// - `sessionToken`: the opaque pointer from a prior `code` exchange,
|
|
41
|
+
// echoed on every refresh once the cookie channel is known unavailable.
|
|
42
|
+
export type RefreshCredential =
|
|
43
|
+
| { kind: 'cookie' }
|
|
44
|
+
| { kind: 'code'; code: string }
|
|
45
|
+
| { kind: 'sessionToken'; sessionToken: string };
|
|
46
|
+
|
|
47
|
+
export interface RefreshResult {
|
|
48
|
+
tokens: TokenSet;
|
|
49
|
+
// Set only for fallback-mode refreshes (`code` / `sessionToken`). The
|
|
50
|
+
// SDK stores this and presents it as the `sessionToken` credential on
|
|
51
|
+
// later refreshes.
|
|
52
|
+
sessionToken?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildSignInUrl(authBaseUrl: string, appId: string, returnTo: string): string {
|
|
56
|
+
const url = new URL(`${authBaseUrl}/sign-in`);
|
|
57
|
+
url.searchParams.set('app_id', appId);
|
|
58
|
+
url.searchParams.set('return_to', returnTo);
|
|
59
|
+
return url.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildSignedOutOfAppUrl(
|
|
63
|
+
authBaseUrl: string,
|
|
64
|
+
appId: string,
|
|
65
|
+
returnTo: string,
|
|
66
|
+
): string {
|
|
67
|
+
const url = new URL(`${authBaseUrl}/signed-out-of-app`);
|
|
68
|
+
url.searchParams.set('app_id', appId);
|
|
69
|
+
url.searchParams.set('return_to', returnTo);
|
|
70
|
+
return url.toString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function refreshTokens(
|
|
74
|
+
authBaseUrl: string,
|
|
75
|
+
appId: string,
|
|
76
|
+
credential: RefreshCredential = { kind: 'cookie' },
|
|
77
|
+
): Promise<RefreshResult | null> {
|
|
78
|
+
// `client` rides a query param, not a header: /refresh is deliberately
|
|
79
|
+
// a CORS simple request (no preflight), and any custom header would
|
|
80
|
+
// trip one. The server folds it into the token_refresh audit so the
|
|
81
|
+
// deployed SDK version distribution is measurable.
|
|
82
|
+
const url = `${authBaseUrl}/refresh?${new URLSearchParams({
|
|
83
|
+
app_id: appId,
|
|
84
|
+
client: SDK_CLIENT_ID,
|
|
85
|
+
}).toString()}`;
|
|
86
|
+
const init: RequestInit = { method: 'POST', credentials: 'include' };
|
|
87
|
+
if (credential.kind !== 'cookie') {
|
|
88
|
+
// A URLSearchParams body sends `Content-Type:
|
|
89
|
+
// application/x-www-form-urlencoded`, which keeps this a CORS
|
|
90
|
+
// "simple request" with no preflight OPTIONS, which the auth API
|
|
91
|
+
// does not serve. A JSON content-type would trip preflight.
|
|
92
|
+
const body = new URLSearchParams();
|
|
93
|
+
if (credential.kind === 'code') body.set('brass_code', credential.code);
|
|
94
|
+
else body.set('sid', credential.sessionToken);
|
|
95
|
+
init.body = body;
|
|
96
|
+
}
|
|
97
|
+
// Retry a transient failure (a cold-start 5xx / 429, or a network blip)
|
|
98
|
+
// rather than collapsing it into the same `null` a definitive 401 returns:
|
|
99
|
+
// `null` signs the user out and clears the login hint, so without a retry one
|
|
100
|
+
// hourly-refresh blip tears down a live session that was fine server-side. A
|
|
101
|
+
// definitive 401/403 is not in either replay set, so it still returns fast.
|
|
102
|
+
// The one-time `brass_code` exchange is single-use server-side, so its
|
|
103
|
+
// channel replays ONLY a pre-handler 503 (never a network throw, which might
|
|
104
|
+
// mean the code was already consumed); the cookie / sid channels are
|
|
105
|
+
// idempotent and replay the full transient set.
|
|
106
|
+
const policy = credential.kind === 'code' ? RETRY_WRITE_503 : RETRY_READ;
|
|
107
|
+
let response: Response;
|
|
108
|
+
try {
|
|
109
|
+
response = await fetchWithRetry(() => fetch(url, init), { policy });
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!response.ok) return null;
|
|
114
|
+
const json = (await response.json()) as RefreshResponse;
|
|
115
|
+
const result: RefreshResult = {
|
|
116
|
+
tokens: {
|
|
117
|
+
accessToken: json.access_token,
|
|
118
|
+
idToken: json.id_token,
|
|
119
|
+
expiresAt: Date.now() + (json.expires_in ?? DEFAULT_TOKEN_TTL_SECONDS) * 1000,
|
|
120
|
+
...(json.app_assertion ? { appAssertion: json.app_assertion } : {}),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
if (json.session_token) result.sessionToken = json.session_token;
|
|
124
|
+
return result;
|
|
125
|
+
}
|