@devaccel-labs/app-comments-toolbar 1.1.0 → 1.3.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 +148 -30
- package/dist/react.js +417 -64
- package/dist/server.js +342 -14
- package/dist/types/capture/api.d.ts +30 -0
- package/dist/types/capture/editor.d.ts +10 -0
- package/dist/types/capture/ignore.d.ts +19 -0
- package/dist/types/capture/index.d.ts +1 -0
- package/dist/types/capture/picker.d.ts +9 -0
- package/dist/types/capture/picture.d.ts +29 -0
- package/dist/types/capture/surface.d.ts +39 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/core/resolve.d.ts +2 -1
- package/dist/types/core/types.d.ts +11 -0
- package/dist/types/debug-context.d.ts +7 -0
- package/dist/types/dictation.d.ts +9 -0
- package/dist/types/embed/controller.d.ts +61 -0
- package/dist/types/embed/index.d.ts +1 -0
- package/dist/types/embed/options.d.ts +35 -0
- package/dist/types/help/index.d.ts +1 -0
- package/dist/types/help/panel-index.d.ts +1 -0
- package/dist/types/help/panel-styles.d.ts +1 -0
- package/dist/types/help/panel.d.ts +4 -0
- package/dist/types/help/pill-styles.d.ts +11 -0
- package/dist/types/help/pill.d.ts +44 -0
- package/dist/types/help/react.d.ts +12 -0
- package/dist/types/help/requests.d.ts +2 -0
- package/dist/types/help/types.d.ts +119 -0
- package/dist/types/help/wire.d.ts +61 -0
- package/dist/types/native-wire.d.ts +63 -0
- package/dist/types/native.d.ts +43 -0
- package/dist/types/react.d.ts +7 -0
- package/dist/types/replay.d.ts +6 -0
- package/dist/types/runtime/api.d.ts +250 -0
- package/dist/types/runtime/attachment-model.d.ts +16 -0
- package/dist/types/runtime/attachments.d.ts +43 -7
- package/dist/types/runtime/boot.d.ts +39 -0
- package/dist/types/runtime/browser-label.d.ts +5 -0
- package/dist/types/runtime/build-id.d.ts +1 -0
- package/dist/types/runtime/capture-ui.d.ts +45 -21
- package/dist/types/runtime/clarity-model.d.ts +260 -0
- package/dist/types/runtime/clarity-ui.d.ts +62 -0
- package/dist/types/runtime/client.d.ts +6 -197
- package/dist/types/runtime/composer-size.d.ts +30 -0
- package/dist/types/runtime/dashboard.d.ts +17 -2
- package/dist/types/runtime/debug-context-core.d.ts +88 -0
- package/dist/types/runtime/debug-context-host.d.ts +39 -0
- package/dist/types/runtime/debug-context-model.d.ts +352 -0
- package/dist/types/runtime/debug-context-redact.d.ts +48 -0
- package/dist/types/runtime/debug-context.d.ts +36 -0
- package/dist/types/runtime/details-model.d.ts +7 -0
- package/dist/types/runtime/details.d.ts +7 -5
- package/dist/types/runtime/dictation-engine.d.ts +98 -0
- package/dist/types/runtime/dictation-model.d.ts +28 -0
- package/dist/types/runtime/dictation-ui.d.ts +56 -0
- package/dist/types/runtime/dictation.d.ts +66 -0
- package/dist/types/runtime/draft-dock.d.ts +32 -0
- package/dist/types/runtime/draft-model.d.ts +104 -0
- package/dist/types/runtime/draft-store.d.ts +15 -0
- package/dist/types/runtime/edits.d.ts +1 -1
- package/dist/types/runtime/font.d.ts +16 -0
- package/dist/types/runtime/format.d.ts +30 -1
- package/dist/types/runtime/host-watch.d.ts +20 -0
- package/dist/types/runtime/icons.d.ts +16 -2
- package/dist/types/runtime/index.d.ts +2 -0
- package/dist/types/runtime/ink.d.ts +8 -0
- package/dist/types/runtime/kind-model.d.ts +14 -0
- package/dist/types/runtime/live-model.d.ts +25 -0
- package/dist/types/runtime/loader.d.ts +5 -3
- package/dist/types/runtime/markup-editor.d.ts +54 -0
- package/dist/types/runtime/markup-model.d.ts +126 -0
- package/dist/types/runtime/markup.d.ts +69 -0
- package/dist/types/runtime/mention-parse.d.ts +18 -0
- package/dist/types/runtime/mentions.d.ts +4 -4
- package/dist/types/runtime/page-background.d.ts +9 -0
- package/dist/types/runtime/page-image.d.ts +64 -11
- package/dist/types/runtime/placement.d.ts +6 -3
- package/dist/types/runtime/record.d.ts +18 -2
- package/dist/types/runtime/related.d.ts +1 -1
- package/dist/types/runtime/replay-model.d.ts +80 -0
- package/dist/types/runtime/replay.d.ts +5 -38
- package/dist/types/runtime/screen.d.ts +47 -0
- package/dist/types/runtime/screenshot.d.ts +60 -8
- package/dist/types/runtime/session-source.d.ts +45 -0
- package/dist/types/runtime/session.d.ts +20 -18
- package/dist/types/runtime/shield.d.ts +9 -0
- package/dist/types/runtime/shot.d.ts +45 -0
- package/dist/types/runtime/sketch.d.ts +26 -0
- package/dist/types/runtime/styles.d.ts +1 -1
- package/dist/types/runtime/sync.d.ts +1 -0
- package/dist/types/runtime/thread-model.d.ts +18 -0
- package/dist/types/runtime/ticket-form-model.d.ts +57 -0
- package/dist/types/runtime/ticket-form.d.ts +9 -0
- package/dist/types/runtime/toolbar.d.ts +37 -1
- package/dist/types/runtime/ui-scale.d.ts +17 -0
- package/dist/types/runtime/update.d.ts +32 -8
- package/dist/types/runtime/use-debug-context.d.ts +27 -0
- package/dist/types/server/index.d.ts +22 -0
- package/dist/types/server/support.d.ts +20 -0
- package/dist/types/server.d.ts +2 -0
- package/dist/types/vite/plugin.d.ts +15 -0
- package/dist/vite.js +121 -41
- package/package.json +24 -11
- package/src/capture/api.ts +128 -0
- package/src/capture/editor.tsx +178 -0
- package/src/capture/ignore.ts +38 -0
- package/src/capture/index.ts +27 -0
- package/src/capture/picker.tsx +171 -0
- package/src/capture/picture.ts +90 -0
- package/src/capture/surface.tsx +185 -0
- package/src/compiler/annotate.ts +202 -0
- package/src/compiler/babel.d.ts +11 -0
- package/src/compiler/enabled.ts +55 -0
- package/src/compiler/index.ts +2 -0
- package/src/core/index.ts +8 -0
- package/src/core/resolve.ts +307 -0
- package/src/core/types.ts +58 -0
- package/src/debug-context.ts +47 -0
- package/src/dictation.ts +17 -0
- package/src/embed/controller.ts +215 -0
- package/src/embed/index.ts +68 -0
- package/src/embed/options.ts +100 -0
- package/src/help/index.ts +38 -0
- package/src/help/panel-index.ts +17 -0
- package/src/help/panel-styles.ts +123 -0
- package/src/help/panel.ts +840 -0
- package/src/help/pill-styles.ts +75 -0
- package/src/help/pill.ts +338 -0
- package/src/help/react.tsx +143 -0
- package/src/help/requests.ts +111 -0
- package/src/help/types.ts +124 -0
- package/src/help/wire.ts +55 -0
- package/src/native-wire.ts +124 -0
- package/src/native.ts +282 -0
- package/src/react.ts +27 -0
- package/src/replay.ts +13 -0
- package/src/runtime/api.ts +531 -0
- package/src/runtime/attachment-model.ts +42 -0
- package/src/runtime/attachments.tsx +1260 -0
- package/src/runtime/boot.ts +95 -0
- package/src/runtime/browser-label.ts +31 -0
- package/src/runtime/build-id.ts +10 -0
- package/src/runtime/capture-ui.tsx +306 -0
- package/src/runtime/capture.ts +508 -0
- package/src/runtime/clarity-model.ts +634 -0
- package/src/runtime/clarity-ui.tsx +454 -0
- package/src/runtime/client.ts +151 -0
- package/src/runtime/composer-size.tsx +146 -0
- package/src/runtime/dashboard.tsx +1130 -0
- package/src/runtime/debug-context-core.ts +681 -0
- package/src/runtime/debug-context-host.ts +56 -0
- package/src/runtime/debug-context-model.ts +1319 -0
- package/src/runtime/debug-context-redact.ts +259 -0
- package/src/runtime/debug-context.ts +613 -0
- package/src/runtime/details-model.ts +30 -0
- package/src/runtime/details.tsx +395 -0
- package/src/runtime/dictation-engine.ts +616 -0
- package/src/runtime/dictation-model.ts +79 -0
- package/src/runtime/dictation-ui.tsx +363 -0
- package/src/runtime/dictation.ts +271 -0
- package/src/runtime/dock.ts +108 -0
- package/src/runtime/draft-dock.tsx +124 -0
- package/src/runtime/draft-model.ts +251 -0
- package/src/runtime/draft-store.ts +149 -0
- package/src/runtime/edits.tsx +165 -0
- package/src/runtime/font.ts +42 -0
- package/src/runtime/format.ts +129 -0
- package/src/runtime/freshness.ts +23 -0
- package/src/runtime/handoff.ts +76 -0
- package/src/runtime/host-watch.ts +64 -0
- package/src/runtime/icons.tsx +295 -0
- package/src/runtime/idle.ts +20 -0
- package/src/runtime/index.ts +11 -0
- package/src/runtime/ink.ts +6 -0
- package/src/runtime/kind-model.ts +20 -0
- package/src/runtime/live-model.ts +32 -0
- package/src/runtime/loader.tsx +87 -0
- package/src/runtime/markup-editor.tsx +1333 -0
- package/src/runtime/markup-model.ts +485 -0
- package/src/runtime/markup.ts +382 -0
- package/src/runtime/members.ts +58 -0
- package/src/runtime/mention-parse.ts +56 -0
- package/src/runtime/mentions.tsx +205 -0
- package/src/runtime/page-background.ts +51 -0
- package/src/runtime/page-image.ts +1592 -0
- package/src/runtime/placement.ts +336 -0
- package/src/runtime/record.ts +402 -0
- package/src/runtime/redact.ts +30 -0
- package/src/runtime/related.ts +34 -0
- package/src/runtime/render-page.ts +2 -0
- package/src/runtime/replay-model.ts +232 -0
- package/src/runtime/replay.ts +173 -0
- package/src/runtime/runtime-version.ts +7 -0
- package/src/runtime/runtime.tsx +51 -0
- package/src/runtime/screen.ts +182 -0
- package/src/runtime/screenshot.ts +260 -0
- package/src/runtime/session-source.ts +112 -0
- package/src/runtime/session.ts +108 -0
- package/src/runtime/shield.ts +34 -0
- package/src/runtime/shot.tsx +422 -0
- package/src/runtime/sketch.ts +139 -0
- package/src/runtime/styles.ts +1541 -0
- package/src/runtime/sync.ts +300 -0
- package/src/runtime/thread-model.ts +71 -0
- package/src/runtime/ticket-form-model.ts +128 -0
- package/src/runtime/ticket-form.tsx +342 -0
- package/src/runtime/toolbar.tsx +4427 -0
- package/src/runtime/ui-scale.ts +75 -0
- package/src/runtime/update.ts +98 -0
- package/src/runtime/use-debug-context.ts +81 -0
- package/src/server/index.ts +133 -0
- package/src/server/support.ts +499 -0
- package/src/server.ts +4 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite.ts +1 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comment service's HTTP API: wire types and request functions. DOM-free, so native clients
|
|
3
|
+
* (`@devaccel-labs/app-comments-native`) share it; `./client.ts` re-exports it with the browser-only sign-in
|
|
4
|
+
* relay and storage helpers.
|
|
5
|
+
*/
|
|
6
|
+
import type { Anchor } from "../core/index.js";
|
|
7
|
+
import { APP_ORIGIN_HEADER } from "../native-wire.js";
|
|
8
|
+
import type { ClarityDraft, ClarityMode } from "./clarity-model.js";
|
|
9
|
+
import type { ReportKind } from "./kind-model.js";
|
|
10
|
+
import { readJwt, type Session } from "./session-source.js";
|
|
11
|
+
import type { TicketDraft, TicketForm } from "./ticket-form-model.js";
|
|
12
|
+
export type Mentioned = { id: string; name: string };
|
|
13
|
+
|
|
14
|
+
/** Where a page screenshot was taken. */
|
|
15
|
+
export type Capture = {
|
|
16
|
+
url: string;
|
|
17
|
+
viewport?: { width: number; height: number };
|
|
18
|
+
browser?: string;
|
|
19
|
+
};
|
|
20
|
+
export type Attachment = {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
contentType: string;
|
|
24
|
+
size: number;
|
|
25
|
+
url: string;
|
|
26
|
+
capture?: Capture;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** An earlier version of a message. `createdAt` is when that version was written. */
|
|
30
|
+
export type Revision = { body: string; mentions?: Mentioned[]; createdAt: string };
|
|
31
|
+
|
|
32
|
+
export type CommentMessage = {
|
|
33
|
+
id: string;
|
|
34
|
+
body: string;
|
|
35
|
+
author: string;
|
|
36
|
+
/** Missing on messages written before author ids were stored. */
|
|
37
|
+
authorId?: string | null;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
editedAt?: string | null;
|
|
40
|
+
mentions?: Mentioned[];
|
|
41
|
+
attachments?: Attachment[];
|
|
42
|
+
/** Earlier versions, oldest first. The current body is the version after the last one. */
|
|
43
|
+
revisions?: Revision[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type Member = { id: string; name: string; email: string };
|
|
47
|
+
|
|
48
|
+
export type Delivery = {
|
|
49
|
+
emailed: Mentioned[];
|
|
50
|
+
skipped: Array<Mentioned & { reason: "rate_limited" | "muted" | "disabled" | string }>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type Profile = { name: string; email: string | null; mentionEmails: boolean };
|
|
54
|
+
|
|
55
|
+
export type PostExtras = {
|
|
56
|
+
mentions?: string[];
|
|
57
|
+
screenshot?: string | null;
|
|
58
|
+
attachments?: string[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type FieldDef = { id: string; label: string };
|
|
62
|
+
export type FieldValue = FieldDef & { value: string };
|
|
63
|
+
export type Settings = {
|
|
64
|
+
roles: { enabled: boolean; options: string[] };
|
|
65
|
+
fields: { enabled: boolean; items: FieldDef[] };
|
|
66
|
+
};
|
|
67
|
+
/** Role and custom field values for a new or edited comment, keyed by field id. */
|
|
68
|
+
export type Details = { role: string | null; fields: Record<string, string> };
|
|
69
|
+
|
|
70
|
+
export type Thread = {
|
|
71
|
+
id: string;
|
|
72
|
+
/** The environment the comment was left in. Differs from the session's for comments from a linked environment. */
|
|
73
|
+
environment?: string;
|
|
74
|
+
route: string;
|
|
75
|
+
/** The page the comment was left on, including query and hash. */
|
|
76
|
+
url: string;
|
|
77
|
+
role: string | null;
|
|
78
|
+
fields: FieldValue[];
|
|
79
|
+
anchor: Anchor;
|
|
80
|
+
status: "open" | "resolved";
|
|
81
|
+
createdBy: string;
|
|
82
|
+
createdAt: string;
|
|
83
|
+
updatedAt: string;
|
|
84
|
+
messages: CommentMessage[];
|
|
85
|
+
/** Set when the team turned this comment into a ticket in their tracker. */
|
|
86
|
+
ticket?: TicketBadge;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** A comment's support ticket, as the commenter sees it: number and status only. */
|
|
90
|
+
export type TicketBadge = { number: string; status: string; label: string };
|
|
91
|
+
|
|
92
|
+
type ErrorBody = { error?: { code?: string; message?: string } };
|
|
93
|
+
|
|
94
|
+
export class ApiError extends Error {
|
|
95
|
+
constructor(
|
|
96
|
+
readonly status: number,
|
|
97
|
+
readonly code: string,
|
|
98
|
+
message: string,
|
|
99
|
+
) {
|
|
100
|
+
super(message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function storageKey(session: Pick<Session, "apiUrl" | "project" | "environment">) {
|
|
105
|
+
return `devaccel.comment.${session.apiUrl}.${session.project}.${session.environment}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Where a client keeps its member token, keyed by `storageKey(session)`. The web toolbar keeps it in the page's
|
|
110
|
+
* `localStorage` itself; native clients pass the device keychain.
|
|
111
|
+
*/
|
|
112
|
+
export type MemberTokenStore = {
|
|
113
|
+
read(key: string): Promise<string | null>;
|
|
114
|
+
write(key: string, token: string): Promise<void>;
|
|
115
|
+
remove(key: string): Promise<void>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** The stored member token for `session`, or null when there is none or it has expired. */
|
|
119
|
+
export async function readMemberToken(
|
|
120
|
+
session: Pick<Session, "apiUrl" | "project" | "environment">,
|
|
121
|
+
store: MemberTokenStore,
|
|
122
|
+
) {
|
|
123
|
+
const token = await store.read(storageKey(session));
|
|
124
|
+
return token && memberIsCurrent(token) ? token : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function memberIsCurrent(token: string) {
|
|
128
|
+
const claims = readJwt(token);
|
|
129
|
+
if (!claims || claims.typ !== "member" || !claims.name || !claims.email) return false;
|
|
130
|
+
return (claims.exp ?? 0) * 1000 > Date.now() + 5_000;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A native client's app origin header (`x-comment-service-origin`). Empty for web, where the browser sends `Origin`. */
|
|
134
|
+
function originHeaders(session: Pick<Session, "clientOrigin">): Record<string, string> {
|
|
135
|
+
return session.clientOrigin ? { [APP_ORIGIN_HEADER]: session.clientOrigin } : {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function request<T>(
|
|
139
|
+
session: Session,
|
|
140
|
+
url: string,
|
|
141
|
+
token: string,
|
|
142
|
+
init?: RequestInit,
|
|
143
|
+
): Promise<T> {
|
|
144
|
+
const response = await fetch(url, {
|
|
145
|
+
...init,
|
|
146
|
+
headers: {
|
|
147
|
+
authorization: `Bearer ${token}`,
|
|
148
|
+
...originHeaders(session),
|
|
149
|
+
...(init?.body ? { "content-type": "application/json" } : {}),
|
|
150
|
+
...(init?.headers ?? {}),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const payload = (await response.json().catch(() => null)) as (T & ErrorBody) | null;
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new ApiError(
|
|
156
|
+
response.status,
|
|
157
|
+
payload?.error?.code ?? "request_failed",
|
|
158
|
+
payload?.error?.message ?? "Request failed.",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return payload as T;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* How people can sign in here: through the service's identity connection, or not at all. Passphrase sign-in is
|
|
166
|
+
* gone. The wire keeps the connection under `msf`, its name from before.
|
|
167
|
+
*/
|
|
168
|
+
export type SignInOptions = {
|
|
169
|
+
passphrase: false;
|
|
170
|
+
/** `label` names the account to sign in with; "your account" when the service names none. */
|
|
171
|
+
msf: { authorizeUrl: string; label: string } | null;
|
|
172
|
+
/** The options request failed. */
|
|
173
|
+
unavailable?: boolean;
|
|
174
|
+
/** The status it failed with, when the service answered. */
|
|
175
|
+
status?: number;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export async function signInOptions(session: Session): Promise<SignInOptions> {
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(`${session.apiUrl}/v1/auth/options`, {
|
|
181
|
+
headers: { authorization: `Bearer ${session.hostToken}`, ...originHeaders(session) },
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok)
|
|
184
|
+
return { passphrase: false, msf: null, unavailable: true, status: response.status };
|
|
185
|
+
const payload = (await response.json()) as Partial<SignInOptions>;
|
|
186
|
+
const msf = payload.msf && typeof payload.msf.authorizeUrl === "string" ? payload.msf : null;
|
|
187
|
+
return {
|
|
188
|
+
passphrase: false,
|
|
189
|
+
msf: msf ? { authorizeUrl: msf.authorizeUrl, label: msf.label || "your account" } : null,
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
return { passphrase: false, msf: null, unavailable: true };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Base64url without padding, as OAuth PKCE wants for the state, verifier and challenge. */
|
|
197
|
+
export function base64Url(bytes: Uint8Array) {
|
|
198
|
+
return btoa(String.fromCharCode(...bytes))
|
|
199
|
+
.replaceAll("+", "-")
|
|
200
|
+
.replaceAll("/", "_")
|
|
201
|
+
.replaceAll("=", "");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The identity connection's authorize URL for an OAuth 2.0 request with PKCE (S256). The sign-in callback sends the code
|
|
206
|
+
* back to `returnTo` (a page of this app, without a hash, or a native app's `<scheme>://app/app-comments/auth`).
|
|
207
|
+
*/
|
|
208
|
+
export function msfAuthorizeUrl(
|
|
209
|
+
authorizeUrl: string,
|
|
210
|
+
returnTo: string,
|
|
211
|
+
state: string,
|
|
212
|
+
challenge: string,
|
|
213
|
+
) {
|
|
214
|
+
const url = new URL(authorizeUrl);
|
|
215
|
+
const redirect = url.searchParams.get("redirect_uri");
|
|
216
|
+
if (redirect) {
|
|
217
|
+
const back = new URL(redirect);
|
|
218
|
+
back.searchParams.set("return", returnTo);
|
|
219
|
+
url.searchParams.set("redirect_uri", back.toString());
|
|
220
|
+
}
|
|
221
|
+
url.searchParams.set("response_type", "code");
|
|
222
|
+
url.searchParams.set("state", state);
|
|
223
|
+
url.searchParams.set("code_challenge", challenge);
|
|
224
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
225
|
+
return url.toString();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Trades the authorization code for a member token. */
|
|
229
|
+
export async function finishMsfSignIn(
|
|
230
|
+
session: Session,
|
|
231
|
+
input: { state: string; verifier: string; code: string },
|
|
232
|
+
) {
|
|
233
|
+
const response = await fetch(`${session.apiUrl}/v1/auth/msf`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers: {
|
|
236
|
+
authorization: `Bearer ${session.hostToken}`,
|
|
237
|
+
"content-type": "application/json",
|
|
238
|
+
...originHeaders(session),
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
hostToken: session.hostToken,
|
|
242
|
+
state: input.state,
|
|
243
|
+
codeVerifier: input.verifier,
|
|
244
|
+
code: input.code,
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
const payload = (await response.json().catch(() => null)) as
|
|
248
|
+
| ({ token?: string } & ErrorBody)
|
|
249
|
+
| null;
|
|
250
|
+
if (!response.ok || !payload?.token) {
|
|
251
|
+
throw new ApiError(
|
|
252
|
+
response.status,
|
|
253
|
+
payload?.error?.code ?? "request_failed",
|
|
254
|
+
payload?.error?.message ?? "Sign-in failed.",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return payload.token;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** What the sign-in window hands back to the app (a code, or an error such as access_denied). */
|
|
261
|
+
export type MsfHandoff = { state: string; code?: string; error?: string; at: number };
|
|
262
|
+
|
|
263
|
+
/** The sign-in result the callback puts in the return address's hash (`cs-msf-state`, `cs-msf-code`, `cs-msf-error`). */
|
|
264
|
+
export function msfHandoffFrom(hash: string, at = Date.now()): MsfHandoff | null {
|
|
265
|
+
const params = new URLSearchParams(hash.startsWith("#") ? hash.slice(1) : hash);
|
|
266
|
+
const state = params.get("cs-msf-state");
|
|
267
|
+
if (!state) return null;
|
|
268
|
+
const code = params.get("cs-msf-code");
|
|
269
|
+
const error = params.get("cs-msf-error");
|
|
270
|
+
return { state, ...(code ? { code } : {}), ...(error ? { error } : {}), at };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function createInvite(session: Session, memberToken: string) {
|
|
274
|
+
return request<{ invite: string; invitedBy: string; expiresAt: string }>(
|
|
275
|
+
session,
|
|
276
|
+
`${session.apiUrl}/v1/invites`,
|
|
277
|
+
memberToken,
|
|
278
|
+
{
|
|
279
|
+
method: "POST",
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function getProfile(session: Session, memberToken: string) {
|
|
285
|
+
return request<Profile>(session, `${session.apiUrl}/v1/me`, memberToken);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function updateProfile(
|
|
289
|
+
session: Session,
|
|
290
|
+
memberToken: string,
|
|
291
|
+
patch: { displayName?: string; email?: string; mentionEmails?: boolean },
|
|
292
|
+
) {
|
|
293
|
+
return request<Profile & { token: string }>(session, `${session.apiUrl}/v1/me`, memberToken, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
body: JSON.stringify(patch),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** `environment` targets a linked environment, for files on a reply to one of its comments. */
|
|
300
|
+
function inEnvironment(url: string, environment?: string) {
|
|
301
|
+
return environment ? `${url}?environment=${encodeURIComponent(environment)}` : url;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function uploadAttachment(
|
|
305
|
+
session: Session,
|
|
306
|
+
memberToken: string,
|
|
307
|
+
file: Blob,
|
|
308
|
+
name: string,
|
|
309
|
+
environment?: string,
|
|
310
|
+
capture?: Capture,
|
|
311
|
+
/** The image was drawn on in the markup editor; the comment service shows it to the title model first. */
|
|
312
|
+
markedUp = false,
|
|
313
|
+
) {
|
|
314
|
+
const response = await fetch(inEnvironment(`${session.apiUrl}/v1/attachments`, environment), {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: {
|
|
317
|
+
authorization: `Bearer ${memberToken}`,
|
|
318
|
+
...originHeaders(session),
|
|
319
|
+
"content-type": file.type || "application/octet-stream",
|
|
320
|
+
"x-file-name": encodeURIComponent(name),
|
|
321
|
+
...(capture ? { "x-capture": encodeURIComponent(JSON.stringify(capture)) } : {}),
|
|
322
|
+
...(markedUp ? { "x-markup": "1" } : {}),
|
|
323
|
+
},
|
|
324
|
+
body: file,
|
|
325
|
+
});
|
|
326
|
+
const payload = (await response.json().catch(() => null)) as
|
|
327
|
+
| ({ attachment: Attachment } & ErrorBody)
|
|
328
|
+
| null;
|
|
329
|
+
if (!response.ok || !payload?.attachment) {
|
|
330
|
+
throw new ApiError(
|
|
331
|
+
response.status,
|
|
332
|
+
payload?.error?.code ?? "upload_failed",
|
|
333
|
+
payload?.error?.message ?? "Upload failed.",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return payload.attachment;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Audio `/v1/transcribe` takes: 16 kHz mono WAV from the web toolbar, m4a (AAC) takes from native clients. */
|
|
340
|
+
const TRANSCRIBE_TYPES: readonly string[] = ["audio/wav", "audio/mp4", "audio/x-m4a"];
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* One dictated piece to text, sent as its blob's type (`audio/wav`, `audio/mp4`, `audio/x-m4a`; WAV when the
|
|
344
|
+
* blob is untyped). `prompt` is the writing just before it, for continuity.
|
|
345
|
+
*/
|
|
346
|
+
export async function transcribeAudio(
|
|
347
|
+
session: Session,
|
|
348
|
+
memberToken: string,
|
|
349
|
+
audio: Blob,
|
|
350
|
+
prompt: string,
|
|
351
|
+
signal: AbortSignal,
|
|
352
|
+
environment?: string,
|
|
353
|
+
) {
|
|
354
|
+
const response = await fetch(inEnvironment(`${session.apiUrl}/v1/transcribe`, environment), {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: {
|
|
357
|
+
authorization: `Bearer ${memberToken}`,
|
|
358
|
+
...originHeaders(session),
|
|
359
|
+
"content-type": TRANSCRIBE_TYPES.includes(audio.type) ? audio.type : "audio/wav",
|
|
360
|
+
...(prompt ? { "x-dictation-prompt": encodeURIComponent(prompt) } : {}),
|
|
361
|
+
},
|
|
362
|
+
body: audio,
|
|
363
|
+
signal,
|
|
364
|
+
});
|
|
365
|
+
const payload = (await response.json().catch(() => null)) as
|
|
366
|
+
| ({ text: string } & ErrorBody)
|
|
367
|
+
| null;
|
|
368
|
+
if (!response.ok || typeof payload?.text !== "string") {
|
|
369
|
+
throw new ApiError(
|
|
370
|
+
response.status,
|
|
371
|
+
payload?.error?.code ?? "transcribe_failed",
|
|
372
|
+
payload?.error?.message ?? "Couldn’t transcribe that.",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return payload.text;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function listMembers(session: Session, memberToken: string, environment?: string) {
|
|
379
|
+
const payload = await request<{ members: Member[] }>(
|
|
380
|
+
session,
|
|
381
|
+
inEnvironment(`${session.apiUrl}/v1/members`, environment),
|
|
382
|
+
memberToken,
|
|
383
|
+
);
|
|
384
|
+
return payload.members;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** "view": this person may read but not comment. Older services don't send it, which means "edit". */
|
|
388
|
+
export type Access = "edit" | "view";
|
|
389
|
+
|
|
390
|
+
export async function listThreads(session: Session, memberToken: string) {
|
|
391
|
+
return request<{
|
|
392
|
+
threads: Thread[];
|
|
393
|
+
settings: Settings;
|
|
394
|
+
environment?: string | null;
|
|
395
|
+
linked?: string[];
|
|
396
|
+
access?: Access;
|
|
397
|
+
/** Set by the project's people settings. A tester comments with the ticket form. */
|
|
398
|
+
memberType?: "client" | "tester";
|
|
399
|
+
/**
|
|
400
|
+
* Whether this environment checks a new comment before it is posted (`./clarity-model.ts`). A service
|
|
401
|
+
* that predates the check doesn't send it, and is never asked to check.
|
|
402
|
+
*/
|
|
403
|
+
clarity?: ClarityMode;
|
|
404
|
+
}>(session, `${session.apiUrl}/v1/threads`, memberToken);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Asks whether a comment that has not been posted yet says enough for a reader to act on. Resolves with the
|
|
409
|
+
* service's answer as it came (`readClarityResponse` reads it) and rejects on any other status; the caller
|
|
410
|
+
* posts the comment either way (`askClarity`).
|
|
411
|
+
*/
|
|
412
|
+
export async function checkClarity(
|
|
413
|
+
session: Session,
|
|
414
|
+
memberToken: string,
|
|
415
|
+
draft: ClarityDraft,
|
|
416
|
+
signal?: AbortSignal,
|
|
417
|
+
) {
|
|
418
|
+
return request<unknown>(session, `${session.apiUrl}/v1/clarity`, memberToken, {
|
|
419
|
+
method: "POST",
|
|
420
|
+
body: JSON.stringify(draft),
|
|
421
|
+
...(signal ? { signal } : {}),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** The tracker's ticket form, for testers only. */
|
|
426
|
+
export async function fetchTicketForm(session: Session, memberToken: string) {
|
|
427
|
+
const payload = await request<{ form: TicketForm }>(
|
|
428
|
+
session,
|
|
429
|
+
`${session.apiUrl}/v1/ticket-form`,
|
|
430
|
+
memberToken,
|
|
431
|
+
);
|
|
432
|
+
return payload.form;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const liveProtocol = "devaccel-comments";
|
|
436
|
+
|
|
437
|
+
/** `WebSocket` as React Native types it: a third argument carries headers. Browsers ignore it. */
|
|
438
|
+
type HeaderSocket = new (
|
|
439
|
+
url: string,
|
|
440
|
+
protocols: string[],
|
|
441
|
+
options?: { headers: Record<string, string> },
|
|
442
|
+
) => WebSocket;
|
|
443
|
+
|
|
444
|
+
/** A socket that says "changed" whenever comments change in this environment or one it links to. */
|
|
445
|
+
export function openLive(session: Session, memberToken: string) {
|
|
446
|
+
const url = new URL("/v1/live", session.apiUrl).href.replace(/^http/, "ws");
|
|
447
|
+
const protocols = [liveProtocol, memberToken];
|
|
448
|
+
if (!session.clientOrigin) return new WebSocket(url, protocols);
|
|
449
|
+
return new (WebSocket as unknown as HeaderSocket)(url, protocols, {
|
|
450
|
+
headers: originHeaders(session),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function saveSettings(session: Session, memberToken: string, settings: Settings) {
|
|
455
|
+
const payload = await request<{ settings: Settings }>(
|
|
456
|
+
session,
|
|
457
|
+
`${session.apiUrl}/v1/settings`,
|
|
458
|
+
memberToken,
|
|
459
|
+
{
|
|
460
|
+
method: "PUT",
|
|
461
|
+
body: JSON.stringify({ settings }),
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
return payload.settings;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export async function createThread(
|
|
468
|
+
session: Session,
|
|
469
|
+
memberToken: string,
|
|
470
|
+
anchor: Anchor,
|
|
471
|
+
body: string,
|
|
472
|
+
extras: PostExtras &
|
|
473
|
+
Partial<Details> & { url?: string; ticket?: TicketDraft; kind?: ReportKind } = {},
|
|
474
|
+
) {
|
|
475
|
+
return request<{ thread: Thread; delivery?: Delivery }>(
|
|
476
|
+
session,
|
|
477
|
+
`${session.apiUrl}/v1/threads`,
|
|
478
|
+
memberToken,
|
|
479
|
+
{
|
|
480
|
+
method: "POST",
|
|
481
|
+
body: JSON.stringify({ anchor, body, ...extras }),
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export async function replyToThread(
|
|
487
|
+
session: Session,
|
|
488
|
+
memberToken: string,
|
|
489
|
+
threadId: string,
|
|
490
|
+
body: string,
|
|
491
|
+
extras: PostExtras = {},
|
|
492
|
+
) {
|
|
493
|
+
return request<{ message: CommentMessage; delivery?: Delivery }>(
|
|
494
|
+
session,
|
|
495
|
+
`${session.apiUrl}/v1/threads/${threadId}/replies`,
|
|
496
|
+
memberToken,
|
|
497
|
+
{
|
|
498
|
+
method: "POST",
|
|
499
|
+
body: JSON.stringify({ body, ...extras }),
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export async function updateThread(
|
|
505
|
+
session: Session,
|
|
506
|
+
memberToken: string,
|
|
507
|
+
threadId: string,
|
|
508
|
+
patch: { status?: "open" | "resolved"; anchor?: Anchor; url?: string } & Partial<Details>,
|
|
509
|
+
) {
|
|
510
|
+
return request<{ id: string }>(session, `${session.apiUrl}/v1/threads/${threadId}`, memberToken, {
|
|
511
|
+
method: "PATCH",
|
|
512
|
+
body: JSON.stringify(patch),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export async function editMessage(
|
|
517
|
+
session: Session,
|
|
518
|
+
memberToken: string,
|
|
519
|
+
threadId: string,
|
|
520
|
+
messageId: string,
|
|
521
|
+
body: string,
|
|
522
|
+
mentions: string[],
|
|
523
|
+
) {
|
|
524
|
+
return request<{
|
|
525
|
+
message: { id: string; body: string; editedAt: string | null };
|
|
526
|
+
delivery: Delivery | null;
|
|
527
|
+
}>(session, `${session.apiUrl}/v1/threads/${threadId}/messages/${messageId}`, memberToken, {
|
|
528
|
+
method: "PATCH",
|
|
529
|
+
body: JSON.stringify({ body, mentions }),
|
|
530
|
+
});
|
|
531
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** How attachments are limited, named and described. DOM-free, so native clients share it. */
|
|
2
|
+
|
|
3
|
+
/** Largest file one message may carry. */
|
|
4
|
+
export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
5
|
+
/** Most files one message may carry. */
|
|
6
|
+
export const ATTACHMENT_MAX_FILES = 5;
|
|
7
|
+
/** The file types a message may carry (the file picker's `accept` list). */
|
|
8
|
+
export const ATTACHMENT_TYPES: readonly string[] = [
|
|
9
|
+
"image/png",
|
|
10
|
+
"image/jpeg",
|
|
11
|
+
"image/gif",
|
|
12
|
+
"image/webp",
|
|
13
|
+
"application/pdf",
|
|
14
|
+
"text/plain",
|
|
15
|
+
"text/csv",
|
|
16
|
+
"application/json",
|
|
17
|
+
"application/zip",
|
|
18
|
+
"video/mp4",
|
|
19
|
+
"video/webm",
|
|
20
|
+
"video/quicktime",
|
|
21
|
+
"audio/webm",
|
|
22
|
+
"audio/mp4",
|
|
23
|
+
"audio/ogg",
|
|
24
|
+
"audio/mpeg",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** `2026-10-31-13-56-37` (UTC): the stamp in `recording-…`, `replay-…` and other attachment names. */
|
|
28
|
+
export function attachmentStamp(at: number | Date) {
|
|
29
|
+
return new Date(at).toISOString().slice(0, 19).replace(/[:T]/g, "-");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `512 B`, `40 KB`, `2.4 MB`. */
|
|
33
|
+
export function formatSize(bytes: number) {
|
|
34
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
35
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
36
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A `replay-*.json` activity file (an instant replay or a clip's timeline). */
|
|
40
|
+
export function isReplayFile(file: { name: string; contentType: string }) {
|
|
41
|
+
return file.contentType === "application/json" && file.name.startsWith("replay-");
|
|
42
|
+
}
|