@cosmoops/connect-sdk 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/LICENSE +34 -0
- package/README.md +446 -0
- package/dist/cjs/cli/bin.js +16 -0
- package/dist/cjs/cli/init.js +179 -0
- package/dist/cjs/client/components/account-sessions.js +112 -0
- package/dist/cjs/client/components/apps-grid.js +46 -0
- package/dist/cjs/client/components/auth-buttons.js +42 -0
- package/dist/cjs/client/components/feature-gate.js +61 -0
- package/dist/cjs/client/components/link-button.js +24 -0
- package/dist/cjs/client/components/logo.js +27 -0
- package/dist/cjs/client/components/plan-pricing.js +166 -0
- package/dist/cjs/client/components/plans-page.js +39 -0
- package/dist/cjs/client/components/plans.js +212 -0
- package/dist/cjs/client/components/readiness.js +117 -0
- package/dist/cjs/client/components/subscribe-dialog.js +60 -0
- package/dist/cjs/client/components/subscription.js +189 -0
- package/dist/cjs/client/components/usage-meter.js +68 -0
- package/dist/cjs/client/components/user-menu.js +172 -0
- package/dist/cjs/client/index.js +99 -0
- package/dist/cjs/client/mark.js +16 -0
- package/dist/cjs/client/provider.js +259 -0
- package/dist/cjs/client/use-checkout.js +173 -0
- package/dist/cjs/errors.js +74 -0
- package/dist/cjs/format-relative-time.js +18 -0
- package/dist/cjs/helpers.js +30 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/next/index.js +311 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/server/client.js +114 -0
- package/dist/cjs/server/handler.js +333 -0
- package/dist/cjs/server/http.js +160 -0
- package/dist/cjs/server/index.js +18 -0
- package/dist/cjs/server/resources/access.js +28 -0
- package/dist/cjs/server/resources/ai.js +36 -0
- package/dist/cjs/server/resources/apps.js +30 -0
- package/dist/cjs/server/resources/billing.js +71 -0
- package/dist/cjs/server/resources/drive.js +39 -0
- package/dist/cjs/server/resources/entitlements.js +49 -0
- package/dist/cjs/server/resources/identity.js +194 -0
- package/dist/cjs/server/resources/logs.js +41 -0
- package/dist/cjs/server/resources/otp.js +29 -0
- package/dist/cjs/server/resources/sessions.js +82 -0
- package/dist/cjs/types.js +15 -0
- package/dist/esm/cli/bin.js +13 -0
- package/dist/esm/cli/init.js +174 -0
- package/dist/esm/client/components/account-sessions.js +109 -0
- package/dist/esm/client/components/apps-grid.js +43 -0
- package/dist/esm/client/components/auth-buttons.js +38 -0
- package/dist/esm/client/components/feature-gate.js +57 -0
- package/dist/esm/client/components/link-button.js +21 -0
- package/dist/esm/client/components/logo.js +24 -0
- package/dist/esm/client/components/plan-pricing.js +156 -0
- package/dist/esm/client/components/plans-page.js +36 -0
- package/dist/esm/client/components/plans.js +209 -0
- package/dist/esm/client/components/readiness.js +113 -0
- package/dist/esm/client/components/subscribe-dialog.js +57 -0
- package/dist/esm/client/components/subscription.js +181 -0
- package/dist/esm/client/components/usage-meter.js +65 -0
- package/dist/esm/client/components/user-menu.js +164 -0
- package/dist/esm/client/index.js +47 -0
- package/dist/esm/client/mark.js +13 -0
- package/dist/esm/client/provider.js +247 -0
- package/dist/esm/client/use-checkout.js +168 -0
- package/dist/esm/errors.js +69 -0
- package/dist/esm/format-relative-time.js +15 -0
- package/dist/esm/helpers.js +26 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/next/index.js +279 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/server/client.js +110 -0
- package/dist/esm/server/handler.js +330 -0
- package/dist/esm/server/http.js +156 -0
- package/dist/esm/server/index.js +10 -0
- package/dist/esm/server/resources/access.js +24 -0
- package/dist/esm/server/resources/ai.js +32 -0
- package/dist/esm/server/resources/apps.js +26 -0
- package/dist/esm/server/resources/billing.js +67 -0
- package/dist/esm/server/resources/drive.js +35 -0
- package/dist/esm/server/resources/entitlements.js +45 -0
- package/dist/esm/server/resources/identity.js +189 -0
- package/dist/esm/server/resources/logs.js +37 -0
- package/dist/esm/server/resources/otp.js +25 -0
- package/dist/esm/server/resources/sessions.js +78 -0
- package/dist/esm/types.js +14 -0
- package/dist/theme.css +84 -0
- package/dist/types/cli/bin.d.ts +1 -0
- package/dist/types/cli/init.d.ts +34 -0
- package/dist/types/client/components/account-sessions.d.ts +25 -0
- package/dist/types/client/components/apps-grid.d.ts +20 -0
- package/dist/types/client/components/auth-buttons.d.ts +31 -0
- package/dist/types/client/components/feature-gate.d.ts +57 -0
- package/dist/types/client/components/link-button.d.ts +20 -0
- package/dist/types/client/components/logo.d.ts +35 -0
- package/dist/types/client/components/plan-pricing.d.ts +96 -0
- package/dist/types/client/components/plans-page.d.ts +36 -0
- package/dist/types/client/components/plans.d.ts +34 -0
- package/dist/types/client/components/readiness.d.ts +52 -0
- package/dist/types/client/components/subscribe-dialog.d.ts +34 -0
- package/dist/types/client/components/subscription.d.ts +123 -0
- package/dist/types/client/components/usage-meter.d.ts +24 -0
- package/dist/types/client/components/user-menu.d.ts +86 -0
- package/dist/types/client/index.d.ts +47 -0
- package/dist/types/client/mark.d.ts +11 -0
- package/dist/types/client/provider.d.ts +209 -0
- package/dist/types/client/use-checkout.d.ts +93 -0
- package/dist/types/errors.d.ts +62 -0
- package/dist/types/format-relative-time.d.ts +2 -0
- package/dist/types/helpers.d.ts +33 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/next/index.d.ts +136 -0
- package/dist/types/server/client.d.ts +83 -0
- package/dist/types/server/handler.d.ts +67 -0
- package/dist/types/server/http.d.ts +52 -0
- package/dist/types/server/index.d.ts +15 -0
- package/dist/types/server/resources/access.d.ts +17 -0
- package/dist/types/server/resources/ai.d.ts +24 -0
- package/dist/types/server/resources/apps.d.ts +12 -0
- package/dist/types/server/resources/billing.d.ts +41 -0
- package/dist/types/server/resources/drive.d.ts +26 -0
- package/dist/types/server/resources/entitlements.d.ts +31 -0
- package/dist/types/server/resources/identity.d.ts +76 -0
- package/dist/types/server/resources/logs.d.ts +20 -0
- package/dist/types/server/resources/otp.d.ts +17 -0
- package/dist/types/server/resources/sessions.d.ts +37 -0
- package/dist/types/types.d.ts +434 -0
- package/package.json +102 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AiResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/** RAG chat and structured extraction against an organization's data. */
|
|
6
|
+
class AiResource {
|
|
7
|
+
transport;
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* RAG-grounded chat. Returns the raw streaming `Response` rather than a
|
|
13
|
+
* parsed body - this endpoint answers with server-sent events, and buffering
|
|
14
|
+
* it here would throw away the only reason to stream. Pipe
|
|
15
|
+
* `response.body` onward, or hand it to the AI SDK's own reader.
|
|
16
|
+
*/
|
|
17
|
+
chat(input, options = {}) {
|
|
18
|
+
return (0, http_js_1.request)(this.transport, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
path: "/ai/chat",
|
|
21
|
+
json: input,
|
|
22
|
+
raw: true,
|
|
23
|
+
signal: options.signal,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Extract structured JSON from a file or image URL, against a platform extraction schema referenced by id. */
|
|
27
|
+
async extract(input) {
|
|
28
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
path: "/ai/extract",
|
|
31
|
+
json: input,
|
|
32
|
+
});
|
|
33
|
+
return result.data;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.AiResource = AiResource;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppsResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/** The services an organization can open - the data behind a launcher grid. */
|
|
6
|
+
class AppsResource {
|
|
7
|
+
transport;
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Launchable apps only, so a grid tile is never a dead end. The calling
|
|
13
|
+
* service is included and flagged `isCurrent` rather than filtered out.
|
|
14
|
+
*/
|
|
15
|
+
async list(organizationId) {
|
|
16
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
17
|
+
method: "GET",
|
|
18
|
+
path: "/apps",
|
|
19
|
+
query: { organizationId },
|
|
20
|
+
});
|
|
21
|
+
// `?? []` for the same reason `billing.listPlans` has it: this is a
|
|
22
|
+
// collection every caller immediately maps or measures, and a body without
|
|
23
|
+
// the key would hand them `undefined`. Worse, the bridge re-wraps this as
|
|
24
|
+
// `{ apps: <value> }` and `JSON.stringify` drops an `undefined` member
|
|
25
|
+
// entirely - so one missing key upstream reaches the browser as `{}` and
|
|
26
|
+
// crashes on `.length`, past every `response.ok` check in between.
|
|
27
|
+
return result.apps ?? [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.AppsResource = AppsResource;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BillingResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
const helpers_js_1 = require("../../helpers.js");
|
|
6
|
+
/** The plan catalog you sell, and metering usage back against a subscriber's add-on balance. */
|
|
7
|
+
class BillingResource {
|
|
8
|
+
transport;
|
|
9
|
+
constructor(transport) {
|
|
10
|
+
this.transport = transport;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Your service's active plans - render live pricing instead of hardcoding it.
|
|
14
|
+
*
|
|
15
|
+
* `features` is normalised on the way out: Connect used to send bare keys
|
|
16
|
+
* and now sends `{ key, name, description }`, and this SDK is versioned
|
|
17
|
+
* independently of the instance it talks to. Without the coercion a partner
|
|
18
|
+
* pointed at a Connect that has not upgraded yet renders a feature list of
|
|
19
|
+
* blanks - which is exactly what happened. See `normalizePlanFeatures`.
|
|
20
|
+
*/
|
|
21
|
+
async listPlans() {
|
|
22
|
+
const response = await (0, http_js_1.request)(this.transport, { method: "GET", path: "/plans" });
|
|
23
|
+
return {
|
|
24
|
+
plans: (response.plans ?? []).map((plan) => ({
|
|
25
|
+
...plan,
|
|
26
|
+
features: (0, helpers_js_1.normalizePlanFeatures)(plan.features),
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What is left of every consumable add-on on an organization's
|
|
32
|
+
* subscription - the read half of `consumeAddon`.
|
|
33
|
+
*
|
|
34
|
+
* Needs a key scoped to `addons:read`, which is separate from
|
|
35
|
+
* `addons:consume` on purpose: a key that only renders a usage meter should
|
|
36
|
+
* not also be able to spend the balance down.
|
|
37
|
+
*
|
|
38
|
+
* Returns an empty `addons` array rather than throwing when there is
|
|
39
|
+
* nothing to meter, so a caller can render it unconditionally.
|
|
40
|
+
*/
|
|
41
|
+
async getAddonUsage(organizationId) {
|
|
42
|
+
const response = await (0, http_js_1.request)(this.transport, {
|
|
43
|
+
method: "GET",
|
|
44
|
+
path: `/addons/organizations/${(0, http_js_1.pathSegment)(organizationId, "organizationId")}`,
|
|
45
|
+
});
|
|
46
|
+
// Normalised, not passed through: the promise above resolves for any 2xx,
|
|
47
|
+
// and `ConnectUsageMeter` reads `addons` straight off it. A body without
|
|
48
|
+
// the key is not an error the meter can see - `data` is truthy, so its
|
|
49
|
+
// `!data` guard passes - and it crashes on `.length` instead of doing what
|
|
50
|
+
// it promises for an empty list, which is to render nothing.
|
|
51
|
+
return { ...response, addons: response.addons ?? [], resetsAt: response.resetsAt ?? null };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Debit a consumable add-on's per-cycle balance on behalf of an
|
|
55
|
+
* organization's users. Call it at whatever cadence your usage happens.
|
|
56
|
+
*
|
|
57
|
+
* An `Idempotency-Key` is always sent, generated per call when you do not
|
|
58
|
+
* supply one: a balance cannot be un-spent, so a retried consume without a
|
|
59
|
+
* key double-debits. Pass your own when the natural unit of work is
|
|
60
|
+
* something you can name (a job id, a message id) and might submit twice.
|
|
61
|
+
*/
|
|
62
|
+
consumeAddon(input, options = {}) {
|
|
63
|
+
return (0, http_js_1.request)(this.transport, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
path: "/addons/consume",
|
|
66
|
+
json: input,
|
|
67
|
+
headers: { "idempotency-key": options.idempotencyKey ?? crypto.randomUUID() },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.BillingResource = BillingResource;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DriveResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/** Files in an organization's Drive - upload, and signed URLs to read back. */
|
|
6
|
+
class DriveResource {
|
|
7
|
+
transport;
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
}
|
|
11
|
+
upload(input) {
|
|
12
|
+
const form = new FormData();
|
|
13
|
+
const fileName = input.fileName ?? input.file.name ?? "upload";
|
|
14
|
+
form.set("file", input.file, fileName);
|
|
15
|
+
form.set("organizationId", input.organizationId);
|
|
16
|
+
if (input.parentId)
|
|
17
|
+
form.set("parentId", input.parentId);
|
|
18
|
+
return (0, http_js_1.request)(this.transport, { method: "POST", path: "/drive/files", multipart: form });
|
|
19
|
+
}
|
|
20
|
+
/** A signed URL for the original bytes. Short-lived - fetch it when you need it, do not store it. */
|
|
21
|
+
async getFileUrl(fileId, input) {
|
|
22
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
23
|
+
method: "GET",
|
|
24
|
+
path: `/drive/files/${(0, http_js_1.pathSegment)(fileId, "fileId")}/url`,
|
|
25
|
+
query: { organizationId: input.organizationId },
|
|
26
|
+
});
|
|
27
|
+
return result.url;
|
|
28
|
+
}
|
|
29
|
+
/** A signed URL for the thumbnail. Same lifetime caveat as `getFileUrl`. */
|
|
30
|
+
async getPreviewUrl(fileId, input) {
|
|
31
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
path: `/drive/files/${(0, http_js_1.pathSegment)(fileId, "fileId")}/preview-url`,
|
|
34
|
+
query: { organizationId: input.organizationId, size: input.size },
|
|
35
|
+
});
|
|
36
|
+
return result.url;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.DriveResource = DriveResource;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EntitlementsResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/**
|
|
6
|
+
* The two checks worth making on every request: may this workspace be here at
|
|
7
|
+
* all, and what is it paying for.
|
|
8
|
+
*/
|
|
9
|
+
class EntitlementsResource {
|
|
10
|
+
transport;
|
|
11
|
+
constructor(transport) {
|
|
12
|
+
this.transport = transport;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every gate Connect owns between an organization and your service -
|
|
16
|
+
* subscription, KYC/KYB, a chargeable payment method, seat headroom -
|
|
17
|
+
* resolved in one call.
|
|
18
|
+
*
|
|
19
|
+
* Blocked users are the point: each blocker carries a `resolveUrl` into the
|
|
20
|
+
* Connect screen that fixes it, so you can route them somewhere useful
|
|
21
|
+
* instead of refusing them with nowhere to go.
|
|
22
|
+
*/
|
|
23
|
+
getReadiness(organizationId) {
|
|
24
|
+
return (0, http_js_1.request)(this.transport, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: `/onboarding/organizations/${(0, http_js_1.pathSegment)(organizationId, "organizationId")}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** The live plan and subscription, including `plan.features`. */
|
|
30
|
+
getSubscription(organizationId) {
|
|
31
|
+
return (0, http_js_1.request)(this.transport, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
path: `/subscriptions/organizations/${(0, http_js_1.pathSegment)(organizationId, "organizationId")}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether an active subscription unlocks a plan feature - the check that
|
|
38
|
+
* gates a plan-restricted module.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately `false` rather than a throw when there is no subscription at
|
|
41
|
+
* all: "not entitled" is the same answer either way, and a feature gate
|
|
42
|
+
* should not need a try/catch around it.
|
|
43
|
+
*/
|
|
44
|
+
async hasFeature(organizationId, feature) {
|
|
45
|
+
const result = await this.getSubscription(organizationId);
|
|
46
|
+
return result.hasActiveSubscription && (result.plan?.features.includes(feature) ?? false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.EntitlementsResource = EntitlementsResource;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdentityResource = void 0;
|
|
4
|
+
exports.createState = createState;
|
|
5
|
+
const errors_js_1 = require("../../errors.js");
|
|
6
|
+
const http_js_1 = require("../http.js");
|
|
7
|
+
/** A URL-safe random string, suitable for `state` or a PKCE verifier. */
|
|
8
|
+
function createState(byteLength = 32) {
|
|
9
|
+
const bytes = new Uint8Array(byteLength);
|
|
10
|
+
crypto.getRandomValues(bytes);
|
|
11
|
+
return base64UrlEncode(bytes);
|
|
12
|
+
}
|
|
13
|
+
function base64UrlEncode(bytes) {
|
|
14
|
+
let binary = "";
|
|
15
|
+
for (const byte of bytes)
|
|
16
|
+
binary += String.fromCharCode(byte);
|
|
17
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
18
|
+
}
|
|
19
|
+
function base64UrlDecode(value) {
|
|
20
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
21
|
+
const binary = atob(padded);
|
|
22
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* "Login with Connect" - the OAuth2 handshake, and the session that outlives
|
|
26
|
+
* it.
|
|
27
|
+
*
|
|
28
|
+
* The whole flow is five calls: `authorizeUrl` to send them out,
|
|
29
|
+
* `exchangeCode` when they come back, `verifyIdToken` once, then `getUser` on
|
|
30
|
+
* each later request and `logout` at the end.
|
|
31
|
+
*/
|
|
32
|
+
class IdentityResource {
|
|
33
|
+
transport;
|
|
34
|
+
clientId;
|
|
35
|
+
appBaseUrl;
|
|
36
|
+
constructor(transport, clientId, appBaseUrl) {
|
|
37
|
+
this.transport = transport;
|
|
38
|
+
this.clientId = clientId;
|
|
39
|
+
this.appBaseUrl = appBaseUrl;
|
|
40
|
+
}
|
|
41
|
+
/** Step 1 - the URL to send the user's browser to. */
|
|
42
|
+
authorizeUrl(input) {
|
|
43
|
+
const url = new URL("/sso/authorize", this.appBaseUrl);
|
|
44
|
+
url.searchParams.set("client_id", this.requireClientId());
|
|
45
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
46
|
+
url.searchParams.set("response_type", "code");
|
|
47
|
+
url.searchParams.set("scope", "openid");
|
|
48
|
+
url.searchParams.set("state", input.state);
|
|
49
|
+
if (input.prompt)
|
|
50
|
+
url.searchParams.set("prompt", input.prompt);
|
|
51
|
+
if (input.codeChallenge) {
|
|
52
|
+
url.searchParams.set("code_challenge", input.codeChallenge);
|
|
53
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
54
|
+
}
|
|
55
|
+
return url.toString();
|
|
56
|
+
}
|
|
57
|
+
/** Step 2 - trade the one-time `code` for a session. Server-side only. */
|
|
58
|
+
async exchangeCode(input) {
|
|
59
|
+
const wire = await this.post("/sso/token", {
|
|
60
|
+
grant_type: "authorization_code",
|
|
61
|
+
code: input.code,
|
|
62
|
+
redirect_uri: input.redirectUri,
|
|
63
|
+
code_verifier: input.codeVerifier,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
idToken: wire.id_token,
|
|
67
|
+
tokenType: wire.token_type,
|
|
68
|
+
expiresIn: wire.expires_in,
|
|
69
|
+
sessionToken: wire.session_token,
|
|
70
|
+
sessionTokenExpiresIn: wire.session_token_expires_in,
|
|
71
|
+
user: wire.user,
|
|
72
|
+
subscription: wire.subscription,
|
|
73
|
+
serviceRole: wire.serviceRole,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Step 3 - verify the `id_token` once, then discard it.
|
|
78
|
+
*
|
|
79
|
+
* HS256 over `header.payload`, keyed with your API key, plus the `exp` and
|
|
80
|
+
* `aud` checks. There is no JWKS to fetch: Connect stores only your key's
|
|
81
|
+
* sha256 hash, so the token is signed with the shared secret rather than an
|
|
82
|
+
* asymmetric key. Signature comparison goes through WebCrypto's own
|
|
83
|
+
* `verify`, which is why this is async - hand-rolling the compare is the
|
|
84
|
+
* step integrations most often get wrong.
|
|
85
|
+
*/
|
|
86
|
+
async verifyIdToken(idToken) {
|
|
87
|
+
// Guarded before `.split`, because the failure this catches is a caller
|
|
88
|
+
// passing something that is not a token at all. Left ungurded it threw a
|
|
89
|
+
// raw TypeError, which is not a `ConnectError`, so every layer above
|
|
90
|
+
// reported it as an unexplained internal error.
|
|
91
|
+
if (typeof idToken !== "string" || idToken.length === 0) {
|
|
92
|
+
throw this.tokenError("id_token is missing");
|
|
93
|
+
}
|
|
94
|
+
const parts = idToken.split(".");
|
|
95
|
+
const [header, payload, signature] = parts;
|
|
96
|
+
if (parts.length !== 3 || !header || !payload || !signature) {
|
|
97
|
+
throw this.tokenError("id_token is not a well-formed JWT");
|
|
98
|
+
}
|
|
99
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(this.transport.apiKey), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
100
|
+
// Decoded before `verify`, and guarded for the same reason the emptiness
|
|
101
|
+
// check above is: `atob` throws a raw `InvalidCharacterError` on a
|
|
102
|
+
// signature that is not base64url, which is not a `ConnectError`, so a
|
|
103
|
+
// caller verifying a token it did not mint gets an unexplained 500 where
|
|
104
|
+
// it should get a clean 401.
|
|
105
|
+
let signatureBytes;
|
|
106
|
+
try {
|
|
107
|
+
signatureBytes = base64UrlDecode(signature);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw this.tokenError("id_token signature is not valid base64url");
|
|
111
|
+
}
|
|
112
|
+
const valid = await crypto.subtle.verify("HMAC", key, signatureBytes, new TextEncoder().encode(`${header}.${payload}`));
|
|
113
|
+
if (!valid)
|
|
114
|
+
throw this.tokenError("id_token signature does not match this API key");
|
|
115
|
+
let claims;
|
|
116
|
+
try {
|
|
117
|
+
claims = JSON.parse(new TextDecoder().decode(base64UrlDecode(payload)));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw this.tokenError("id_token payload is not valid JSON");
|
|
121
|
+
}
|
|
122
|
+
if (typeof claims.exp !== "number" || claims.exp * 1000 <= Date.now()) {
|
|
123
|
+
throw this.tokenError("id_token has expired");
|
|
124
|
+
}
|
|
125
|
+
if (claims.aud !== this.requireClientId()) {
|
|
126
|
+
throw this.tokenError("id_token was issued for a different client_id");
|
|
127
|
+
}
|
|
128
|
+
return claims;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Step 4 - who is signed in, right now. Same blocks as `exchangeCode`,
|
|
132
|
+
* re-resolved live, so a role change or a lapsed subscription shows up here
|
|
133
|
+
* without another sign-in.
|
|
134
|
+
*
|
|
135
|
+
* Any `ConnectError` with `isSessionExpired` means send them back through
|
|
136
|
+
* `authorizeUrl()`.
|
|
137
|
+
*/
|
|
138
|
+
async getUser(sessionToken) {
|
|
139
|
+
const wire = await this.post("/sso/user", { session_token: sessionToken });
|
|
140
|
+
return {
|
|
141
|
+
user: wire.user,
|
|
142
|
+
subscription: wire.subscription,
|
|
143
|
+
serviceRole: wire.serviceRole,
|
|
144
|
+
sessionExpiresAt: wire.session_expires_at,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Step 5 - end the sign-in immediately rather than leaving it valid for the
|
|
149
|
+
* rest of its ~15-day life. Discard your own copy of `sessionToken` too;
|
|
150
|
+
* this does not do that for you.
|
|
151
|
+
*
|
|
152
|
+
* `scope` defaults to `"device"`: this sign-in, every other service signed
|
|
153
|
+
* into from the same browser, and Connect's own session on that one device.
|
|
154
|
+
* `"all"` ends every session on every device - an explicit "sign out
|
|
155
|
+
* everywhere" action only.
|
|
156
|
+
*
|
|
157
|
+
* Always succeeds. An unknown or already-revoked token is an
|
|
158
|
+
* indistinguishable no-op, so there is nothing to branch on.
|
|
159
|
+
*/
|
|
160
|
+
async logout(sessionToken, options = {}) {
|
|
161
|
+
await this.post("/sso/logout", {
|
|
162
|
+
session_token: sessionToken,
|
|
163
|
+
scope: options.scope,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
post(path, form) {
|
|
167
|
+
const spec = {
|
|
168
|
+
method: "POST",
|
|
169
|
+
path,
|
|
170
|
+
auth: "none",
|
|
171
|
+
form: {
|
|
172
|
+
...form,
|
|
173
|
+
client_id: this.requireClientId(),
|
|
174
|
+
client_secret: this.transport.apiKey,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
return (0, http_js_1.request)(this.transport, spec);
|
|
178
|
+
}
|
|
179
|
+
requireClientId() {
|
|
180
|
+
if (!this.clientId) {
|
|
181
|
+
throw new errors_js_1.ConnectError({
|
|
182
|
+
status: 0,
|
|
183
|
+
code: "missing_client_id",
|
|
184
|
+
message: "identity.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this service's Setup tab once SSO is enabled.",
|
|
185
|
+
endpoint: "identity",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return this.clientId;
|
|
189
|
+
}
|
|
190
|
+
tokenError(message) {
|
|
191
|
+
return new errors_js_1.ConnectError({ status: 401, code: "invalid_token", message, endpoint: "verifyIdToken" });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
exports.IdentityResource = IdentityResource;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogsResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/** Push feature events into the Console's audit log, filterable to your service. */
|
|
6
|
+
class LogsResource {
|
|
7
|
+
transport;
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Never throws. A logging call that takes down the code path it was
|
|
13
|
+
* observing is worse than a lost line, so a failure here is swallowed and
|
|
14
|
+
* reported through the return value.
|
|
15
|
+
*
|
|
16
|
+
* Pass `sessionToken` to attribute the event to the end user it belongs to.
|
|
17
|
+
* Both scopes on the stored entry come from something verified - the service
|
|
18
|
+
* from your API key, the user from that token, which Connect checks was
|
|
19
|
+
* minted for your service before attributing anything. `attributed` in the
|
|
20
|
+
* result says whether it resolved; a `false` there still means the line was
|
|
21
|
+
* stored, just without a user on it.
|
|
22
|
+
*/
|
|
23
|
+
async write(input) {
|
|
24
|
+
const timestamp = input.timestamp instanceof Date ? input.timestamp.toISOString() : input.timestamp;
|
|
25
|
+
try {
|
|
26
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
path: "/logs",
|
|
29
|
+
json: { ...input, timestamp },
|
|
30
|
+
});
|
|
31
|
+
// `?? false` covers a Connect old enough to predate the field - an
|
|
32
|
+
// absent `attributed` means the server had no attribution to report,
|
|
33
|
+
// which is exactly what `false` says.
|
|
34
|
+
return { delivered: true, attributed: result?.attributed ?? false };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { delivered: false, attributed: false };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.LogsResource = LogsResource;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OtpResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/** WhatsApp/SMS phone verification. */
|
|
6
|
+
class OtpResource {
|
|
7
|
+
transport;
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Send a code. As with `billing.consumeAddon`, an `Idempotency-Key` always
|
|
13
|
+
* goes along - a retried send would otherwise deliver a second message and
|
|
14
|
+
* invalidate the first code the user is looking at.
|
|
15
|
+
*/
|
|
16
|
+
send(input, options = {}) {
|
|
17
|
+
return (0, http_js_1.request)(this.transport, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
path: "/otp/send",
|
|
20
|
+
json: input,
|
|
21
|
+
headers: { "idempotency-key": options.idempotencyKey ?? crypto.randomUUID() },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Check a 6-digit code against a `verificationId`. */
|
|
25
|
+
verify(input) {
|
|
26
|
+
return (0, http_js_1.request)(this.transport, { method: "POST", path: "/otp/verify", json: input });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.OtpResource = OtpResource;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionsResource = void 0;
|
|
4
|
+
const errors_js_1 = require("../../errors.js");
|
|
5
|
+
const http_js_1 = require("../http.js");
|
|
6
|
+
/**
|
|
7
|
+
* This Connect account's own login sessions - device, approximate location,
|
|
8
|
+
* timestamps - the same list Connect's own account settings page shows the
|
|
9
|
+
* user themselves.
|
|
10
|
+
*
|
|
11
|
+
* **Account-wide, not scoped to this service.** A Connect session is one
|
|
12
|
+
* browser's login to Connect, and it fans out to every partner service's own
|
|
13
|
+
* `sessionToken` - Connect has no per-service session concept to filter by.
|
|
14
|
+
* `list()` returns every session on the account, including ones that
|
|
15
|
+
* originated from a completely different, unrelated service. Requires the
|
|
16
|
+
* `sessions:manage` scope on your API key - unlike `identity`'s base `sso`
|
|
17
|
+
* capability, this is never granted to a zero-scope legacy key by default.
|
|
18
|
+
*
|
|
19
|
+
* Authenticated like `identity.*` - `client_id`+`client_secret`, never a
|
|
20
|
+
* Bearer key - because every call here is really "resolve identity from a
|
|
21
|
+
* `sessionToken`", the same shape `identity.getUser`/`identity.logout` are.
|
|
22
|
+
*/
|
|
23
|
+
class SessionsResource {
|
|
24
|
+
transport;
|
|
25
|
+
clientId;
|
|
26
|
+
constructor(transport, clientId) {
|
|
27
|
+
this.transport = transport;
|
|
28
|
+
this.clientId = clientId;
|
|
29
|
+
}
|
|
30
|
+
/** Every active session on the account behind `sessionToken`. `isCurrent` marks the one it was minted from. */
|
|
31
|
+
async list(sessionToken) {
|
|
32
|
+
const result = await this.post("/sso/sessions", {
|
|
33
|
+
session_token: sessionToken,
|
|
34
|
+
});
|
|
35
|
+
// See `AppsResource.list` - a collection unwrap has to survive a body
|
|
36
|
+
// that omits the key, or it becomes a browser-side crash two hops away.
|
|
37
|
+
return result.items ?? [];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Ends one specific session by id - not necessarily the one `sessionToken`
|
|
41
|
+
* itself was minted from. Cascades to every partner-service sign-in that
|
|
42
|
+
* originated from that same browser, same as Connect's own "sign out this
|
|
43
|
+
* device".
|
|
44
|
+
*/
|
|
45
|
+
async revoke(sessionToken, sessionId) {
|
|
46
|
+
await this.post("/sso/sessions/revoke", {
|
|
47
|
+
session_token: sessionToken,
|
|
48
|
+
sessionId,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** "Sign out everywhere else" - keeps the session `sessionToken` was minted from, ends every other one. */
|
|
52
|
+
async revokeOthers(sessionToken) {
|
|
53
|
+
return this.post("/sso/sessions/revoke-others", {
|
|
54
|
+
session_token: sessionToken,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
post(path, form) {
|
|
58
|
+
const spec = {
|
|
59
|
+
method: "POST",
|
|
60
|
+
path,
|
|
61
|
+
auth: "none",
|
|
62
|
+
form: {
|
|
63
|
+
...form,
|
|
64
|
+
client_id: this.requireClientId(),
|
|
65
|
+
client_secret: this.transport.apiKey,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return (0, http_js_1.request)(this.transport, spec);
|
|
69
|
+
}
|
|
70
|
+
requireClientId() {
|
|
71
|
+
if (!this.clientId) {
|
|
72
|
+
throw new errors_js_1.ConnectError({
|
|
73
|
+
status: 0,
|
|
74
|
+
code: "missing_client_id",
|
|
75
|
+
message: "sessions.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this service's Setup tab once SSO is enabled.",
|
|
76
|
+
endpoint: "sessions",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return this.clientId;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.SessionsResource = SessionsResource;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Wire types for the `/api/v1/service/*` surface - the service-partner API,
|
|
4
|
+
* and the only thing this SDK covers.
|
|
5
|
+
*
|
|
6
|
+
* Hand-written rather than generated so the published package carries no
|
|
7
|
+
* dependency on Connect's internals. `src/conformance.test.ts` type-checks
|
|
8
|
+
* these against the server's own DTOs, so drift fails the build here instead
|
|
9
|
+
* of surfacing as a runtime surprise in a partner's integration.
|
|
10
|
+
*
|
|
11
|
+
* Dates cross the wire as ISO 8601 strings and are left that way - parsing
|
|
12
|
+
* them into `Date` would be a lossy guess about what the caller wants, and
|
|
13
|
+
* `new Date(value)` is right there.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `connect-sdk` executable.
|
|
3
|
+
*
|
|
4
|
+
* A thin wrapper so `init.ts` stays importable and testable - a module that
|
|
5
|
+
* calls `process.exit` at import time cannot be unit tested, and this is the
|
|
6
|
+
* only file that does.
|
|
7
|
+
*
|
|
8
|
+
* The shebang is added by `scripts/finalize-dist.ts`: TypeScript strips leading
|
|
9
|
+
* comments it does not recognise as directives, and a `bin` without one is run
|
|
10
|
+
* by the shell rather than by Node.
|
|
11
|
+
*/
|
|
12
|
+
import { main } from "./init.js";
|
|
13
|
+
process.exit(main(process.argv.slice(2)));
|