@flow-industries/id 0.26.5 → 0.27.1

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.
@@ -197,6 +197,11 @@
197
197
  "type": false,
198
198
  "value": true
199
199
  },
200
+ {
201
+ "name": "createGitHubConnector",
202
+ "type": false,
203
+ "value": true
204
+ },
200
205
  {
201
206
  "name": "createNoteTransport",
202
207
  "type": false,
@@ -367,6 +372,86 @@
367
372
  "type": false,
368
373
  "value": true
369
374
  },
375
+ {
376
+ "name": "GITHUB_BASELINE_SCOPES",
377
+ "type": false,
378
+ "value": true
379
+ },
380
+ {
381
+ "name": "GITHUB_REQUESTABLE_SCOPES",
382
+ "type": false,
383
+ "value": true
384
+ },
385
+ {
386
+ "name": "GITHUB_SCOPE_DESCRIPTIONS",
387
+ "type": false,
388
+ "value": true
389
+ },
390
+ {
391
+ "name": "GitHubConnector",
392
+ "type": true,
393
+ "value": false
394
+ },
395
+ {
396
+ "name": "GitHubConnectorCallOptions",
397
+ "type": true,
398
+ "value": false
399
+ },
400
+ {
401
+ "name": "GitHubConnectorError",
402
+ "type": true,
403
+ "value": true
404
+ },
405
+ {
406
+ "name": "GitHubConnectorErrorCode",
407
+ "type": true,
408
+ "value": false
409
+ },
410
+ {
411
+ "name": "GitHubConnectorFailure",
412
+ "type": true,
413
+ "value": false
414
+ },
415
+ {
416
+ "name": "GitHubConnectorOptions",
417
+ "type": true,
418
+ "value": false
419
+ },
420
+ {
421
+ "name": "GitHubConnectorResult",
422
+ "type": true,
423
+ "value": false
424
+ },
425
+ {
426
+ "name": "githubConsentUrl",
427
+ "type": false,
428
+ "value": true
429
+ },
430
+ {
431
+ "name": "GitHubOperationData",
432
+ "type": true,
433
+ "value": false
434
+ },
435
+ {
436
+ "name": "GitHubOperationId",
437
+ "type": true,
438
+ "value": false
439
+ },
440
+ {
441
+ "name": "GitHubOperationInput",
442
+ "type": true,
443
+ "value": false
444
+ },
445
+ {
446
+ "name": "GitHubScope",
447
+ "type": true,
448
+ "value": false
449
+ },
450
+ {
451
+ "name": "hasGitHubScope",
452
+ "type": false,
453
+ "value": true
454
+ },
370
455
  {
371
456
  "name": "hitLabel",
372
457
  "type": false,
@@ -0,0 +1,19 @@
1
+ import { type GitHubScope } from "../github/scopes";
2
+ import type { GitHubConnector, GitHubConnectorErrorCode, GitHubConnectorFailure, GitHubConnectorOptions } from "../types/github-connector";
3
+ /** A refused connector call. `consentUrl` is set when the user can fix it in
4
+ * Flow ID: connecting GitHub, reconnecting, or granting `requiredScopes`. */
5
+ export declare class GitHubConnectorError extends Error {
6
+ readonly status: number;
7
+ readonly code: GitHubConnectorErrorCode;
8
+ readonly requiredScopes: GitHubScope[];
9
+ readonly consentUrl?: string;
10
+ readonly retryAfter?: number;
11
+ readonly fields: string[];
12
+ constructor(status: number, failure: GitHubConnectorFailure, options?: ErrorOptions);
13
+ }
14
+ /**
15
+ * Calls GitHub through Flow ID as the signed-in user. Flow ID holds the
16
+ * user's GitHub grant and answers with projected data, so the app never sees
17
+ * a GitHub token. The app's origin must be on Flow ID's connector allow-list.
18
+ */
19
+ export declare function createGitHubConnector(host: string, getToken: () => Promise<string | null>, getUserId: () => string | null, options?: GitHubConnectorOptions): GitHubConnector;
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import { githubConsentUrl, isGitHubScope, } from "../github/scopes";
3
+ /** A refused connector call. `consentUrl` is set when the user can fix it in
4
+ * Flow ID: connecting GitHub, reconnecting, or granting `requiredScopes`. */
5
+ export class GitHubConnectorError extends Error {
6
+ status;
7
+ code;
8
+ requiredScopes;
9
+ consentUrl;
10
+ retryAfter;
11
+ fields;
12
+ constructor(status, failure, options) {
13
+ super(failure.code, options);
14
+ this.status = status;
15
+ this.name = "GitHubConnectorError";
16
+ this.code = failure.code;
17
+ this.requiredScopes = failure.requiredScopes ?? [];
18
+ this.consentUrl = failure.consentUrl;
19
+ this.retryAfter = failure.retryAfter;
20
+ this.fields = failure.fields ?? [];
21
+ }
22
+ }
23
+ const failureSchema = z.object({
24
+ code: z.string().max(64),
25
+ requiredScopes: z.array(z.string().max(64)).max(16).optional(),
26
+ consentUrl: z.string().max(2048).optional(),
27
+ retryAfter: z.number().int().min(0).max(3600).optional(),
28
+ fields: z.array(z.string().max(256)).max(20).optional(),
29
+ });
30
+ const knownCodes = new Set([
31
+ "GITHUB_CONNECTOR_DISABLED",
32
+ "ORIGIN_DENIED",
33
+ "SIGN_IN_REQUIRED",
34
+ "ACCOUNT_CHANGED",
35
+ "RATE_LIMITED",
36
+ "UNKNOWN_OPERATION",
37
+ "INVALID_BODY",
38
+ "TOO_LARGE",
39
+ "GITHUB_NOT_CONNECTED",
40
+ "GITHUB_RECONNECT_REQUIRED",
41
+ "GITHUB_SCOPE_REQUIRED",
42
+ "GITHUB_RATE_LIMITED",
43
+ "GITHUB_NOT_FOUND",
44
+ "GITHUB_FORBIDDEN",
45
+ "GITHUB_CONFLICT",
46
+ "GITHUB_VALIDATION_FAILED",
47
+ "GITHUB_UNAVAILABLE",
48
+ "GITHUB_INVALID_RESPONSE",
49
+ ]);
50
+ function isKnownCode(code) {
51
+ return knownCodes.has(code);
52
+ }
53
+ const successSchema = z.object({
54
+ data: z.unknown(),
55
+ nextPage: z.number().int().min(1).max(1000).nullable(),
56
+ });
57
+ /**
58
+ * Calls GitHub through Flow ID as the signed-in user. Flow ID holds the
59
+ * user's GitHub grant and answers with projected data, so the app never sees
60
+ * a GitHub token. The app's origin must be on Flow ID's connector allow-list.
61
+ */
62
+ export function createGitHubConnector(host, getToken, getUserId, options = {}) {
63
+ // Never call `fetch` as a method of another object: Chrome throws
64
+ // "Illegal invocation" for any receiver other than the window.
65
+ const send = options.fetch ??
66
+ ((input, init) => globalThis.fetch(input, init));
67
+ const consentUrl = (scopes = []) => githubConsentUrl(host, scopes);
68
+ return {
69
+ async call(operation, input, callOptions = {}) {
70
+ const userId = getUserId();
71
+ const token = userId ? await getToken() : null;
72
+ if (!userId || !token)
73
+ throw new GitHubConnectorError(401, { code: "SIGN_IN_REQUIRED" });
74
+ let response;
75
+ try {
76
+ response = await send(`${host}/api/connectors/github/${encodeURIComponent(operation)}`, {
77
+ method: "POST",
78
+ headers: {
79
+ Authorization: `Bearer ${token}`,
80
+ "Content-Type": "application/json",
81
+ "X-Flow-User-Id": userId,
82
+ },
83
+ body: JSON.stringify(input),
84
+ signal: callOptions.signal,
85
+ });
86
+ }
87
+ catch (error) {
88
+ callOptions.signal?.throwIfAborted();
89
+ throw new GitHubConnectorError(0, { code: "CONNECTOR_UNAVAILABLE" }, {
90
+ cause: error,
91
+ });
92
+ }
93
+ const payload = await response.json().catch(() => null);
94
+ if (getUserId() !== userId)
95
+ throw new GitHubConnectorError(409, { code: "ACCOUNT_CHANGED" });
96
+ if (!response.ok) {
97
+ const failure = failureSchema.safeParse(payload);
98
+ throw new GitHubConnectorError(response.status, failure.success && isKnownCode(failure.data.code)
99
+ ? {
100
+ ...failure.data,
101
+ code: failure.data.code,
102
+ requiredScopes: failure.data.requiredScopes?.filter(isGitHubScope),
103
+ }
104
+ : { code: "CONNECTOR_UNAVAILABLE" });
105
+ }
106
+ const success = successSchema.safeParse(payload);
107
+ if (!success.success)
108
+ throw new GitHubConnectorError(502, {
109
+ code: "GITHUB_INVALID_RESPONSE",
110
+ });
111
+ /* SAFETY: Flow ID validated `data` against this operation's response schema
112
+ before answering; the SDK names that contract rather than re-validating it. */
113
+ return success.data;
114
+ },
115
+ consentUrl,
116
+ requestScopes(scopes = []) {
117
+ if (!("window" in globalThis))
118
+ return null;
119
+ // Open blank first so the opener link is cut before Flow ID loads.
120
+ const opened = window.open("about:blank", "_blank");
121
+ if (!opened)
122
+ return null;
123
+ opened.opener = null;
124
+ opened.location.href = consentUrl(scopes);
125
+ return opened;
126
+ },
127
+ };
128
+ }
@@ -1,5 +1,7 @@
1
+ export { GITHUB_BASELINE_SCOPES, GITHUB_REQUESTABLE_SCOPES, GITHUB_SCOPE_DESCRIPTIONS, githubConsentUrl, hasGitHubScope, } from "../github/scopes";
1
2
  export { defaultIdHost, isLocalHostname } from "../id-host";
2
3
  export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, AtprotoApi, AtprotoResolution, AtprotoResolutionFailure, AtprotoResolveOptions, BrowserAccount, BrowserAccounts, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MarkNotificationsReadInput, MethodName, MountFlowWidgetOptions, MountProfileOptions, NotificationCounts, NotificationEntry, NotificationPage, NotificationReadResult, NotificationRefusal, OpenProfileOptions, PasskeyPrfOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, SwitchAccountOptions, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
4
+ export type { GitHubConnector, GitHubConnectorCallOptions, GitHubConnectorErrorCode, GitHubConnectorFailure, GitHubConnectorOptions, GitHubConnectorResult, GitHubOperationData, GitHubOperationId, GitHubOperationInput, GitHubScope, } from "../types/github-connector";
3
5
  export type { NoteBlob, NoteCollection, NoteOperation, NoteRecord, NoteResponses, NoteStoredRecord, NoteVaultHeader, } from "../types/note-transport";
4
6
  export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
5
7
  export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
@@ -8,6 +10,7 @@ export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
8
10
  export { createDialogHost } from "./dialog-host";
9
11
  export { createFlowWidget } from "./flow-widget";
10
12
  export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
13
+ export { createGitHubConnector, GitHubConnectorError, } from "./github-connector";
11
14
  export { METHODS } from "./methods";
12
15
  export { createNoteTransport, NoteTransportError } from "./note-transport";
13
16
  export { closeProfile, openProfile } from "./open-profile";
@@ -1,3 +1,4 @@
1
+ export { GITHUB_BASELINE_SCOPES, GITHUB_REQUESTABLE_SCOPES, GITHUB_SCOPE_DESCRIPTIONS, githubConsentUrl, hasGitHubScope, } from "../github/scopes";
1
2
  export { defaultIdHost, isLocalHostname } from "../id-host";
2
3
  export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
3
4
  export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
@@ -6,6 +7,7 @@ export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
6
7
  export { createDialogHost } from "./dialog-host";
7
8
  export { createFlowWidget } from "./flow-widget";
8
9
  export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
10
+ export { createGitHubConnector, GitHubConnectorError, } from "./github-connector";
9
11
  export { METHODS } from "./methods";
10
12
  export { createNoteTransport, NoteTransportError } from "./note-transport";
11
13
  export { closeProfile, openProfile } from "./open-profile";
@@ -0,0 +1,9 @@
1
+ import type { OAuthCompletion, OAuthStartRequest, OAuthStatus } from "../types";
2
+ /** The provider a hosted-page or account-page URL is returning from, if any. */
3
+ export declare function oauthReturnProvider(search: string): string | null;
4
+ export declare function getOAuthStatus(provider: string, signal?: AbortSignal): Promise<OAuthStatus>;
5
+ export declare function unlinkOAuth(provider: string, expectedUserId: string): Promise<void>;
6
+ /** Starts authorization and leaves the page for the provider; resolves only on failure paths. */
7
+ export declare function startOAuth(provider: string, request: OAuthStartRequest, signal: AbortSignal): Promise<void>;
8
+ /** Finishes the attempt this tab started; memoized so a re-render cannot spend the token twice. */
9
+ export declare function completeOAuth(provider: string): Promise<OAuthCompletion>;
@@ -0,0 +1,68 @@
1
+ /** The provider a hosted-page or account-page URL is returning from, if any. */
2
+ export function oauthReturnProvider(search) {
3
+ return new URLSearchParams(search).get("oauth");
4
+ }
5
+ const endpoint = (provider, action) => `/api/auth/oauth/${encodeURIComponent(provider)}/${action}`;
6
+ const completionKey = (provider) => `flow.oauth.completion.${provider}`;
7
+ async function readResponse(response) {
8
+ const body = await response.json().catch(() => ({}));
9
+ if (!response.ok)
10
+ throw new Error(body.message ?? body.error ?? "Could not connect your account.");
11
+ // SAFETY: the caller names the payload returned by the same-origin Flow endpoint.
12
+ return body;
13
+ }
14
+ export async function getOAuthStatus(provider, signal) {
15
+ return readResponse(await fetch(endpoint(provider, "status"), {
16
+ credentials: "include",
17
+ signal,
18
+ }));
19
+ }
20
+ export async function unlinkOAuth(provider, expectedUserId) {
21
+ await readResponse(await fetch(endpoint(provider, "unlink"), {
22
+ method: "POST",
23
+ credentials: "include",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({ expectedUserId }),
26
+ }));
27
+ }
28
+ /** Starts authorization and leaves the page for the provider; resolves only on failure paths. */
29
+ export async function startOAuth(provider, request, signal) {
30
+ signal.throwIfAborted();
31
+ const result = await readResponse(await fetch(endpoint(provider, "start"), {
32
+ method: "POST",
33
+ credentials: "include",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify(request),
36
+ signal,
37
+ }));
38
+ signal.throwIfAborted();
39
+ sessionStorage.setItem(completionKey(provider), result.completionToken);
40
+ window.location.assign(result.url);
41
+ }
42
+ const completions = new Map();
43
+ /** Finishes the attempt this tab started; memoized so a re-render cannot spend the token twice. */
44
+ export function completeOAuth(provider) {
45
+ const existing = completions.get(provider);
46
+ if (existing)
47
+ return existing;
48
+ const completionToken = sessionStorage.getItem(completionKey(provider));
49
+ if (!completionToken)
50
+ return Promise.reject(new Error("Sign-in expired. Please try again."));
51
+ const completion = fetch(endpoint(provider, "complete"), {
52
+ method: "POST",
53
+ credentials: "include",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({ completionToken }),
56
+ })
57
+ .then(async (response) => {
58
+ if (response.status === 202)
59
+ throw new Error("Authorization is still finishing. Please try again.");
60
+ return readResponse(response);
61
+ })
62
+ .then((result) => {
63
+ sessionStorage.removeItem(completionKey(provider));
64
+ return result;
65
+ });
66
+ completions.set(provider, completion);
67
+ return completion;
68
+ }