@dewee/identity 0.0.0-stage → 0.1.0-beta.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.
- package/README.md +95 -2
- package/dist/assertion.d.ts +64 -0
- package/dist/assertion.js +155 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +2 -0
- package/dist/errors.d.ts +66 -0
- package/dist/errors.js +174 -0
- package/dist/exchange.d.ts +51 -0
- package/dist/exchange.js +74 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/job-access.d.ts +26 -0
- package/dist/job-access.js +182 -0
- package/dist/lifecycle.d.ts +50 -0
- package/dist/lifecycle.js +67 -0
- package/dist/links.d.ts +99 -0
- package/dist/links.js +140 -0
- package/dist/transport.d.ts +40 -0
- package/dist/transport.js +103 -0
- package/dist/verify-trusted-user.d.ts +76 -0
- package/dist/verify-trusted-user.js +173 -0
- package/package.json +53 -4
package/dist/exchange.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createAssertion, requireUuid } from "./assertion.js";
|
|
2
|
+
import { DeweeIdentityError, invalidArgument } from "./errors.js";
|
|
3
|
+
import { assertServer, gatewayRequest, requireString } from "./transport.js";
|
|
4
|
+
/** Authorization scheme of partner server calls. */
|
|
5
|
+
export const ASSERTION_AUTH_SCHEME = "Dewee-Assertion";
|
|
6
|
+
/** Header that carries an identity session token on HTTP calls. */
|
|
7
|
+
export const IDENTITY_SESSION_HEADER = "X-Dewee-Identity-Session";
|
|
8
|
+
/** Bounds of an identity session lifetime. */
|
|
9
|
+
export const MIN_SESSION_TTL_SECONDS = 60;
|
|
10
|
+
export const MAX_SESSION_TTL_SECONDS = 43_200;
|
|
11
|
+
function checkTtl(ttl) {
|
|
12
|
+
if (ttl === undefined)
|
|
13
|
+
return;
|
|
14
|
+
if (!Number.isInteger(ttl) || ttl < MIN_SESSION_TTL_SECONDS || ttl > MAX_SESSION_TTL_SECONDS) {
|
|
15
|
+
throw invalidArgument(`ttlSeconds must be an integer from ${MIN_SESSION_TTL_SECONDS} to ${MAX_SESSION_TTL_SECONDS}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Exchanges a partner proof of one user for a short-lived identity session bound to one agent
|
|
20
|
+
* (`POST /v1/identity/sessions/exchange`). The tenant and application come from the verified
|
|
21
|
+
* assertion; nothing in the request names a user, tenant or role.
|
|
22
|
+
*/
|
|
23
|
+
export async function exchangeSession(opts) {
|
|
24
|
+
assertServer();
|
|
25
|
+
if (opts === null || typeof opts !== "object")
|
|
26
|
+
throw invalidArgument("options are required");
|
|
27
|
+
if (typeof opts.agent !== "string" || opts.agent.trim() === "" || opts.agent.length > 255) {
|
|
28
|
+
throw invalidArgument("agent is required: the agent id or key");
|
|
29
|
+
}
|
|
30
|
+
checkTtl(opts.ttlSeconds);
|
|
31
|
+
let assertion;
|
|
32
|
+
const body = { agent_id: opts.agent.trim() };
|
|
33
|
+
if ("signer" in opts && opts.signer !== undefined) {
|
|
34
|
+
assertion = await createAssertion({ ...opts.signer, purpose: "session", subject: opts.subject });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const withAssertion = opts;
|
|
38
|
+
if (typeof withAssertion.assertion !== "string" || withAssertion.assertion === "") {
|
|
39
|
+
throw invalidArgument("assertion is required (or pass signer and subject)");
|
|
40
|
+
}
|
|
41
|
+
assertion = withAssertion.assertion;
|
|
42
|
+
if (withAssertion.oidcApplicationId !== undefined) {
|
|
43
|
+
body.id_token = true;
|
|
44
|
+
body.application_id = requireUuid(withAssertion.oidcApplicationId, "oidcApplicationId");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (opts.ttlSeconds !== undefined)
|
|
48
|
+
body.ttl_seconds = opts.ttlSeconds;
|
|
49
|
+
const path = "/v1/identity/sessions/exchange";
|
|
50
|
+
const { body: res } = await gatewayRequest(opts, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
path,
|
|
53
|
+
headers: { Authorization: `${ASSERTION_AUTH_SCHEME} ${assertion}` },
|
|
54
|
+
body,
|
|
55
|
+
});
|
|
56
|
+
const expiresAt = new Date(requireString(res, "expires_at", path));
|
|
57
|
+
if (Number.isNaN(expiresAt.getTime())) {
|
|
58
|
+
throw new DeweeIdentityError({ code: "INVALID_RESPONSE", message: `INVALID_RESPONSE: ${path} answer has a bad expires_at` });
|
|
59
|
+
}
|
|
60
|
+
const session = {
|
|
61
|
+
sessionToken: requireString(res, "session_token", path),
|
|
62
|
+
header: typeof res.header === "string" && res.header !== "" ? res.header : IDENTITY_SESSION_HEADER,
|
|
63
|
+
sessionId: requireString(res, "session_id", path),
|
|
64
|
+
principalId: requireString(res, "principal_id", path),
|
|
65
|
+
applicationId: requireString(res, "application_id", path),
|
|
66
|
+
agentId: requireString(res, "agent_id", path),
|
|
67
|
+
expiresAt,
|
|
68
|
+
testMode: res.test_mode === true,
|
|
69
|
+
};
|
|
70
|
+
if (typeof res.test_mode_banner === "string" && res.test_mode_banner !== "") {
|
|
71
|
+
session.testModeBanner = res.test_mode_banner;
|
|
72
|
+
}
|
|
73
|
+
return session;
|
|
74
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dewee/identity: the server SDK of Dewee Connect.
|
|
3
|
+
*
|
|
4
|
+
* - {@link exchangeSession}: sign a user in to a Dewee agent (widget session).
|
|
5
|
+
* - {@link createConnectRedirect}: answer the hosted-link handoff of Dewee Connect.
|
|
6
|
+
* - {@link verifyTrustedUser}: inside your MCP server, read the verified user of a call.
|
|
7
|
+
*
|
|
8
|
+
* Server only: every entry point refuses to run where `window` exists.
|
|
9
|
+
*/
|
|
10
|
+
export { ASSERTION_TYPES, DEFAULT_ASSERTION_TTL_SECONDS, MAX_ASSERTION_TTL_SECONDS, MIN_SECRET_LENGTH, applicationAudience, createAssertion, type AssertionPurpose, type AssertionSigner, type AsymmetricAlgorithm, type CreateAssertionOptions, type LifecycleEvent, } from "./assertion.js";
|
|
11
|
+
export { DeweeIdentityError, GATEWAY_ERROR_CODES, IDENTITY_ERROR_CODES, IDENTITY_ERROR_HINTS, REMEDIATION_ACTIONS, REMEDIATION_AUDIENCES, SDK_ERROR_CODES, isIdentityErrorCode, type DeweeIdentityErrorCode, type GatewayErrorCode, type IdentityErrorCode, type Remediation, type RemediationAction, type RemediationAudience, type SdkErrorCode, type TrustedUserRefusal, } from "./errors.js";
|
|
12
|
+
export { ASSERTION_AUTH_SCHEME, IDENTITY_SESSION_HEADER, MAX_SESSION_TTL_SECONDS, MIN_SESSION_TTL_SECONDS, exchangeSession, type ExchangeSessionOptions, type ExchangeWithAssertion, type ExchangeWithSigner, type IdentitySession, } from "./exchange.js";
|
|
13
|
+
export { lifecycle, logout, revokeSession, type LifecycleOptions, type LifecycleResult, type LogoutOptions, type RevokeSessionOptions, } from "./lifecycle.js";
|
|
14
|
+
export { CONNECT_RETURN_PATH, checkConnectRequest, createConnectRedirect, readConnectRequest, startLink, type ConnectRedirect, type ConnectRequest, type CreateConnectRedirectOptions, type LinkTarget, type ReadConnectRequestOptions, type StartLinkOptions, type StartedLink, } from "./links.js";
|
|
15
|
+
export { type GatewayOptions } from "./transport.js";
|
|
16
|
+
export { MemoryReplayGuard, TRUSTED_USER_CLOCK_SKEW_SECONDS, TRUSTED_USER_ISSUER_PREFIX, TRUSTED_USER_TOKEN_TTL_SECONDS, TRUSTED_USER_TOKEN_TYPE, extractTrustedUserToken, verifyTrustedUser, type ReplayGuard, type TrustedUser, type VerifyTrustedUserOptions, } from "./verify-trusted-user.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dewee/identity: the server SDK of Dewee Connect.
|
|
3
|
+
*
|
|
4
|
+
* - {@link exchangeSession}: sign a user in to a Dewee agent (widget session).
|
|
5
|
+
* - {@link createConnectRedirect}: answer the hosted-link handoff of Dewee Connect.
|
|
6
|
+
* - {@link verifyTrustedUser}: inside your MCP server, read the verified user of a call.
|
|
7
|
+
*
|
|
8
|
+
* Server only: every entry point refuses to run where `window` exists.
|
|
9
|
+
*/
|
|
10
|
+
export { ASSERTION_TYPES, DEFAULT_ASSERTION_TTL_SECONDS, MAX_ASSERTION_TTL_SECONDS, MIN_SECRET_LENGTH, applicationAudience, createAssertion, } from "./assertion.js";
|
|
11
|
+
export { DeweeIdentityError, GATEWAY_ERROR_CODES, IDENTITY_ERROR_CODES, IDENTITY_ERROR_HINTS, REMEDIATION_ACTIONS, REMEDIATION_AUDIENCES, SDK_ERROR_CODES, isIdentityErrorCode, } from "./errors.js";
|
|
12
|
+
export { ASSERTION_AUTH_SCHEME, IDENTITY_SESSION_HEADER, MAX_SESSION_TTL_SECONDS, MIN_SESSION_TTL_SECONDS, exchangeSession, } from "./exchange.js";
|
|
13
|
+
export { lifecycle, logout, revokeSession, } from "./lifecycle.js";
|
|
14
|
+
export { CONNECT_RETURN_PATH, checkConnectRequest, createConnectRedirect, readConnectRequest, startLink, } from "./links.js";
|
|
15
|
+
export { MemoryReplayGuard, TRUSTED_USER_CLOCK_SKEW_SECONDS, TRUSTED_USER_ISSUER_PREFIX, TRUSTED_USER_TOKEN_TTL_SECONDS, TRUSTED_USER_TOKEN_TYPE, extractTrustedUserToken, verifyTrustedUser, } from "./verify-trusted-user.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type JobAccessErrorCode = "INVALID_ARGUMENT" | "BROWSER_REQUIRED" | "POPUP_BLOCKED" | "POPUP_CLOSED" | "TIMEOUT" | "CANCELLED" | "IDENTITY_REQUIRED" | "LINK_EXPIRED" | "LINK_REVOKED" | "PERMISSION_DENIED" | "SOURCE_UNAVAILABLE" | "BOOTSTRAP_FAILED";
|
|
2
|
+
/** Fixed, secret-free errors; remote error text is never copied. */
|
|
3
|
+
export declare class JobAccessError extends Error {
|
|
4
|
+
readonly code: JobAccessErrorCode;
|
|
5
|
+
constructor(code: JobAccessErrorCode);
|
|
6
|
+
toJSON(): {
|
|
7
|
+
code: JobAccessErrorCode;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface OpenJobAccessOptions {
|
|
12
|
+
/** Public Gateway origin, HTTPS (HTTP is allowed only on localhost). */
|
|
13
|
+
gatewayUrl: string;
|
|
14
|
+
/** Original verified identity session, held in memory. Never a visitor or Gateway token. */
|
|
15
|
+
identitySession: string;
|
|
16
|
+
/** Cancels only this launch, not an already opened authenticated page. */
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
/** Handshake deadline, 1–120 seconds. Default 30 seconds. */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Call directly from a human click. Opens the fixed hosted page synchronously;
|
|
23
|
+
* resolves after authenticated bootstrap, before any separate grant consent.
|
|
24
|
+
* No token is put in a URL, storage, DOM, logs or the returned value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function openJobAccess(options: OpenJobAccessOptions): Promise<void>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const messages = {
|
|
2
|
+
INVALID_ARGUMENT: "Valid gateway origin and identity session are required.",
|
|
3
|
+
BROWSER_REQUIRED: "Open job access from a browser.",
|
|
4
|
+
POPUP_BLOCKED: "Allow popups, then open job access again.",
|
|
5
|
+
POPUP_CLOSED: "Job access was closed. Open it again to continue.",
|
|
6
|
+
TIMEOUT: "Job access did not respond. Open it again to retry.",
|
|
7
|
+
CANCELLED: "Opening job access was cancelled.",
|
|
8
|
+
IDENTITY_REQUIRED: "Sign in again, then open job access.",
|
|
9
|
+
LINK_EXPIRED: "Your sign-in expired. Sign in again, then open job access.",
|
|
10
|
+
LINK_REVOKED: "Your sign-in was revoked. Sign in again, then open job access.",
|
|
11
|
+
PERMISSION_DENIED: "This account cannot open job access.",
|
|
12
|
+
SOURCE_UNAVAILABLE: "Job access is temporarily unavailable. Try again later.",
|
|
13
|
+
BOOTSTRAP_FAILED: "Job access could not be opened. Try again.",
|
|
14
|
+
};
|
|
15
|
+
/** Fixed, secret-free errors; remote error text is never copied. */
|
|
16
|
+
export class JobAccessError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code) {
|
|
19
|
+
super(messages[code]);
|
|
20
|
+
this.name = "JobAccessError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
toJSON() {
|
|
24
|
+
return { code: this.code, message: this.message };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const remoteCodes = new Set([
|
|
28
|
+
"IDENTITY_REQUIRED", "LINK_EXPIRED", "LINK_REVOKED", "PERMISSION_DENIED",
|
|
29
|
+
"SOURCE_UNAVAILABLE", "BOOTSTRAP_FAILED",
|
|
30
|
+
]);
|
|
31
|
+
function exactKeys(data, keys) {
|
|
32
|
+
return Object.keys(data).length === keys.length && keys.every((key) => Object.hasOwn(data, key));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Call directly from a human click. Opens the fixed hosted page synchronously;
|
|
36
|
+
* resolves after authenticated bootstrap, before any separate grant consent.
|
|
37
|
+
* No token is put in a URL, storage, DOM, logs or the returned value.
|
|
38
|
+
*/
|
|
39
|
+
export function openJobAccess(options) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const fail = (code) => reject(new JobAccessError(code));
|
|
42
|
+
if (typeof window === "undefined") {
|
|
43
|
+
fail("BROWSER_REQUIRED");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!options || typeof options !== "object") {
|
|
47
|
+
fail("INVALID_ARGUMENT");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let token = options.identitySession;
|
|
51
|
+
const signal = options.signal;
|
|
52
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
53
|
+
let origin;
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(options.gatewayUrl);
|
|
56
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
57
|
+
if ((url.protocol !== "https:" && !(url.protocol === "http:" && local)) ||
|
|
58
|
+
url.username || url.password || url.search || url.hash || url.pathname !== "/" ||
|
|
59
|
+
typeof token !== "string" || !token.trim() || token.length > 16_384 ||
|
|
60
|
+
(signal !== undefined && (typeof AbortSignal === "undefined" || !(signal instanceof AbortSignal))) ||
|
|
61
|
+
!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 120_000) {
|
|
62
|
+
fail("INVALID_ARGUMENT");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
origin = url.origin;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
fail("INVALID_ARGUMENT");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (signal?.aborted) {
|
|
72
|
+
token = undefined;
|
|
73
|
+
fail("CANCELLED");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let popup;
|
|
77
|
+
try {
|
|
78
|
+
popup = window.open(`${origin}/identity/job-access?launch=1`, "_blank", "popup,width=720,height=800");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
token = undefined;
|
|
82
|
+
fail("POPUP_BLOCKED");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!popup) {
|
|
86
|
+
token = undefined;
|
|
87
|
+
fail("POPUP_BLOCKED");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const target = popup;
|
|
91
|
+
let nonce;
|
|
92
|
+
let settled = false;
|
|
93
|
+
let interval;
|
|
94
|
+
let timeout;
|
|
95
|
+
const finish = (code) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
token = undefined;
|
|
100
|
+
nonce = undefined;
|
|
101
|
+
clearInterval(interval);
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
window.removeEventListener("message", receive);
|
|
104
|
+
window.removeEventListener("pagehide", cancel);
|
|
105
|
+
try {
|
|
106
|
+
signal?.removeEventListener("abort", cancel);
|
|
107
|
+
}
|
|
108
|
+
catch { /* Best-effort cancellation cleanup. */ }
|
|
109
|
+
if (code) {
|
|
110
|
+
try {
|
|
111
|
+
target.close();
|
|
112
|
+
}
|
|
113
|
+
catch { /* Cleanup cannot override the safe error. */ }
|
|
114
|
+
fail(code);
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
resolve();
|
|
118
|
+
};
|
|
119
|
+
const cancel = () => finish("CANCELLED");
|
|
120
|
+
const receive = (event) => {
|
|
121
|
+
if (settled || event.source !== target || event.origin !== origin ||
|
|
122
|
+
!event.data || typeof event.data !== "object" || Array.isArray(event.data))
|
|
123
|
+
return;
|
|
124
|
+
const data = event.data;
|
|
125
|
+
if (data.version !== 1)
|
|
126
|
+
return;
|
|
127
|
+
if (!nonce && data.type === "dewee.job-access.ready" &&
|
|
128
|
+
exactKeys(data, ["type", "version", "nonce"]) && typeof data.nonce === "string" &&
|
|
129
|
+
/^[A-Za-z0-9_-]{22,128}$/.test(data.nonce)) {
|
|
130
|
+
nonce = data.nonce;
|
|
131
|
+
try {
|
|
132
|
+
target.postMessage({ type: "dewee.job-access.session", version: 1, nonce, identitySession: token }, origin);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
finish("BOOTSTRAP_FAILED");
|
|
136
|
+
}
|
|
137
|
+
token = undefined;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!nonce || data.type !== "dewee.job-access.result" || data.nonce !== nonce)
|
|
141
|
+
return;
|
|
142
|
+
if (data.status === "ready" && exactKeys(data, ["type", "version", "nonce", "status"]))
|
|
143
|
+
finish();
|
|
144
|
+
else if (data.status === "error" && exactKeys(data, ["type", "version", "nonce", "status", "code"]) &&
|
|
145
|
+
typeof data.code === "string" && remoteCodes.has(data.code)) {
|
|
146
|
+
finish(data.code);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const hello = () => {
|
|
150
|
+
if (settled)
|
|
151
|
+
return;
|
|
152
|
+
if (target.closed) {
|
|
153
|
+
finish("POPUP_CLOSED");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!nonce) {
|
|
157
|
+
try {
|
|
158
|
+
target.postMessage({ type: "dewee.job-access.hello", version: 1 }, origin);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
finish("BOOTSTRAP_FAILED");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
window.addEventListener("message", receive);
|
|
166
|
+
window.addEventListener("pagehide", cancel);
|
|
167
|
+
try {
|
|
168
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
finish("INVALID_ARGUMENT");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (signal?.aborted) {
|
|
175
|
+
finish("CANCELLED");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
interval = setInterval(hello, 200);
|
|
179
|
+
timeout = setTimeout(() => finish("TIMEOUT"), timeoutMs);
|
|
180
|
+
hello();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type AssertionSigner, type LifecycleEvent } from "./assertion.js";
|
|
2
|
+
import { type GatewayOptions } from "./transport.js";
|
|
3
|
+
/** Options of every partner call made with a lifecycle assertion. */
|
|
4
|
+
interface PartnerCall extends GatewayOptions {
|
|
5
|
+
signer: AssertionSigner;
|
|
6
|
+
/** The user the call is about: your stable user id, as in the session assertion. */
|
|
7
|
+
subject: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LifecycleOptions extends PartnerCall {
|
|
10
|
+
event: LifecycleEvent;
|
|
11
|
+
/** With `scopes_changed` only: the user's complete new scope list. */
|
|
12
|
+
scopes?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface LifecycleResult {
|
|
15
|
+
principalId: string;
|
|
16
|
+
event: string;
|
|
17
|
+
status: "active" | "suspended" | "deleted";
|
|
18
|
+
grantVersion: number;
|
|
19
|
+
sessionsRevoked: number;
|
|
20
|
+
bindingsRevoked: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reports a lifecycle event of one of your users (`POST /v1/identity/principals/lifecycle`):
|
|
24
|
+
* `suspend` and `delete` revoke the user's sessions at once and `delete` also unlinks every chat
|
|
25
|
+
* account; `scopes_changed` replaces the scopes. Repeating an event already applied is a no-op, so
|
|
26
|
+
* a retry is safe. The event and scopes travel inside the signed assertion, not in the body.
|
|
27
|
+
*/
|
|
28
|
+
export declare function lifecycle(opts: LifecycleOptions): Promise<LifecycleResult>;
|
|
29
|
+
export interface RevokeSessionOptions extends PartnerCall {
|
|
30
|
+
/** The `sessionId` of an issued session of `subject`. */
|
|
31
|
+
sessionId: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Revokes one identity session of your user (`POST /v1/identity/sessions/{id}/revoke`). A session
|
|
35
|
+
* of another user, or of another application, answers `NOT_FOUND`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function revokeSession(opts: RevokeSessionOptions): Promise<{
|
|
38
|
+
sessionId: string;
|
|
39
|
+
revoked: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
export interface LogoutOptions extends PartnerCall {
|
|
42
|
+
/** The `principalId` Dewee returned with the user's session. */
|
|
43
|
+
principalId: string;
|
|
44
|
+
}
|
|
45
|
+
/** Revokes every identity session of your user (`POST /v1/identity/principals/{id}/logout`). */
|
|
46
|
+
export declare function logout(opts: LogoutOptions): Promise<{
|
|
47
|
+
principalId: string;
|
|
48
|
+
sessionsRevoked: number;
|
|
49
|
+
}>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createAssertion, requireUuid } from "./assertion.js";
|
|
2
|
+
import { invalidArgument } from "./errors.js";
|
|
3
|
+
import { ASSERTION_AUTH_SCHEME } from "./exchange.js";
|
|
4
|
+
import { assertServer, gatewayRequest } from "./transport.js";
|
|
5
|
+
async function lifecycleAuthorization(opts, event, scopes) {
|
|
6
|
+
const assertion = await createAssertion({ ...opts.signer, purpose: "lifecycle", subject: opts.subject, event, scopes });
|
|
7
|
+
return `${ASSERTION_AUTH_SCHEME} ${assertion}`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reports a lifecycle event of one of your users (`POST /v1/identity/principals/lifecycle`):
|
|
11
|
+
* `suspend` and `delete` revoke the user's sessions at once and `delete` also unlinks every chat
|
|
12
|
+
* account; `scopes_changed` replaces the scopes. Repeating an event already applied is a no-op, so
|
|
13
|
+
* a retry is safe. The event and scopes travel inside the signed assertion, not in the body.
|
|
14
|
+
*/
|
|
15
|
+
export async function lifecycle(opts) {
|
|
16
|
+
assertServer();
|
|
17
|
+
if (opts === null || typeof opts !== "object")
|
|
18
|
+
throw invalidArgument("options are required");
|
|
19
|
+
if (opts.event === undefined)
|
|
20
|
+
throw invalidArgument("event is required");
|
|
21
|
+
const authorization = await lifecycleAuthorization(opts, opts.event, opts.scopes);
|
|
22
|
+
const { body } = await gatewayRequest(opts, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
path: "/v1/identity/principals/lifecycle",
|
|
25
|
+
headers: { Authorization: authorization },
|
|
26
|
+
});
|
|
27
|
+
const status = body.status;
|
|
28
|
+
return {
|
|
29
|
+
principalId: String(body.principal_id ?? ""),
|
|
30
|
+
event: String(body.event ?? opts.event),
|
|
31
|
+
status: status === "suspended" || status === "deleted" ? status : "active",
|
|
32
|
+
grantVersion: Number(body.grant_version ?? 0),
|
|
33
|
+
sessionsRevoked: Number(body.sessions_revoked ?? 0),
|
|
34
|
+
bindingsRevoked: Number(body.bindings_revoked ?? 0),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Revokes one identity session of your user (`POST /v1/identity/sessions/{id}/revoke`). A session
|
|
39
|
+
* of another user, or of another application, answers `NOT_FOUND`.
|
|
40
|
+
*/
|
|
41
|
+
export async function revokeSession(opts) {
|
|
42
|
+
assertServer();
|
|
43
|
+
if (opts === null || typeof opts !== "object")
|
|
44
|
+
throw invalidArgument("options are required");
|
|
45
|
+
const id = requireUuid(opts.sessionId, "sessionId");
|
|
46
|
+
const authorization = await lifecycleAuthorization(opts);
|
|
47
|
+
const { body } = await gatewayRequest(opts, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
path: `/v1/identity/sessions/${id}/revoke`,
|
|
50
|
+
headers: { Authorization: authorization },
|
|
51
|
+
});
|
|
52
|
+
return { sessionId: String(body.session_id ?? id), revoked: body.revoked === true };
|
|
53
|
+
}
|
|
54
|
+
/** Revokes every identity session of your user (`POST /v1/identity/principals/{id}/logout`). */
|
|
55
|
+
export async function logout(opts) {
|
|
56
|
+
assertServer();
|
|
57
|
+
if (opts === null || typeof opts !== "object")
|
|
58
|
+
throw invalidArgument("options are required");
|
|
59
|
+
const id = requireUuid(opts.principalId, "principalId");
|
|
60
|
+
const authorization = await lifecycleAuthorization(opts);
|
|
61
|
+
const { body } = await gatewayRequest(opts, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
path: `/v1/identity/principals/${id}/logout`,
|
|
64
|
+
headers: { Authorization: authorization },
|
|
65
|
+
});
|
|
66
|
+
return { principalId: String(body.principal_id ?? id), sessionsRevoked: Number(body.sessions_revoked ?? 0) };
|
|
67
|
+
}
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type AssertionSigner } from "./assertion.js";
|
|
2
|
+
import { type GatewayOptions } from "./transport.js";
|
|
3
|
+
/** Path of the hosted Connect page that receives the partner handoff. */
|
|
4
|
+
export declare const CONNECT_RETURN_PATH = "/connect/return";
|
|
5
|
+
export interface ConnectRequest {
|
|
6
|
+
/** The ceremony reference; it is the nonce the handoff assertion must carry. */
|
|
7
|
+
deweeTxn: string;
|
|
8
|
+
/** Where to send the user back: always `{connectBaseUrl}/connect/return`. */
|
|
9
|
+
returnTo: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ReadConnectRequestOptions {
|
|
12
|
+
/** Your Dewee Connect origin (`GOCLAW_IDENTITY_CONNECT_BASE_URL`, else the gateway public URL). */
|
|
13
|
+
connectBaseUrl: string;
|
|
14
|
+
/** Allow a plain-http connect origin that is not loopback (private networks only). */
|
|
15
|
+
allowInsecureHttp?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Checks the two values Dewee sends to your connect start URL. `return_to` must be exactly your
|
|
19
|
+
* Dewee Connect page: anything else is refused, so a crafted link can never make your server hand
|
|
20
|
+
* a signed assertion to another site.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkConnectRequest(input: {
|
|
23
|
+
deweeTxn: unknown;
|
|
24
|
+
returnTo: unknown;
|
|
25
|
+
}, opts: ReadConnectRequestOptions): ConnectRequest;
|
|
26
|
+
/** Reads and checks `dewee_txn` and `return_to` from the URL of a request to your connect start URL. */
|
|
27
|
+
export declare function readConnectRequest(requestUrl: string | URL, opts: ReadConnectRequestOptions): ConnectRequest;
|
|
28
|
+
export interface CreateConnectRedirectOptions extends ReadConnectRequestOptions {
|
|
29
|
+
signer: AssertionSigner;
|
|
30
|
+
/** The signed-in user on your side: your stable user id. */
|
|
31
|
+
subject: string;
|
|
32
|
+
/** From {@link readConnectRequest}. */
|
|
33
|
+
deweeTxn: string;
|
|
34
|
+
/** From {@link readConnectRequest}. */
|
|
35
|
+
returnTo: string;
|
|
36
|
+
/** Assertion lifetime in seconds (default 60, at most 300). */
|
|
37
|
+
ttlSeconds?: number;
|
|
38
|
+
/**
|
|
39
|
+
* The nonce of your page's `Content-Security-Policy` (`script-src 'nonce-…'`). Without it a
|
|
40
|
+
* strict CSP blocks the inline auto-submit and the user presses the Continue button instead.
|
|
41
|
+
*/
|
|
42
|
+
cspNonce?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ConnectRedirect {
|
|
45
|
+
/** POST target: your Dewee Connect return page. */
|
|
46
|
+
action: string;
|
|
47
|
+
method: "POST";
|
|
48
|
+
/** Form fields to POST (the signed handoff assertion). */
|
|
49
|
+
fields: {
|
|
50
|
+
assertion: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A complete HTML page that POSTs the fields (scripted submit, with a button as fallback). Send it
|
|
54
|
+
* with `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. Under a strict CSP pass
|
|
55
|
+
* `cspNonce`, or the user has to press the button.
|
|
56
|
+
*/
|
|
57
|
+
html: string;
|
|
58
|
+
/**
|
|
59
|
+
* GET fallback URL with the assertion in the query. Prefer the POST form: a URL can end up in
|
|
60
|
+
* logs and history.
|
|
61
|
+
*/
|
|
62
|
+
location: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Builds the hosted-link handoff: signs a handoff assertion for the signed-in user, bound to this
|
|
66
|
+
* ceremony (`nonce` = `dewee_txn`), and returns the POST form that sends the user back to Dewee
|
|
67
|
+
* Connect. Dewee then shows the consent screen and the chat proof step.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createConnectRedirect(opts: CreateConnectRedirectOptions): Promise<ConnectRedirect>;
|
|
70
|
+
export interface StartLinkOptions extends GatewayOptions {
|
|
71
|
+
/** The user's identity session token (from {@link exchangeSession}). */
|
|
72
|
+
sessionToken: string;
|
|
73
|
+
/** Makes a retry return the same ceremony instead of starting a second one. */
|
|
74
|
+
idempotencyKey?: string;
|
|
75
|
+
}
|
|
76
|
+
export interface LinkTarget {
|
|
77
|
+
channelType: string;
|
|
78
|
+
channelInstanceId: string;
|
|
79
|
+
name: string;
|
|
80
|
+
displayName?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface StartedLink {
|
|
83
|
+
transactionId: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
state: string;
|
|
86
|
+
/** Show it to the user exactly as given; they send it in a direct chat from the account to link. */
|
|
87
|
+
proofCode: string;
|
|
88
|
+
expiresAt: Date;
|
|
89
|
+
/** false when `idempotencyKey` replayed an earlier start. */
|
|
90
|
+
created: boolean;
|
|
91
|
+
/** The chat accounts the code can be sent to. */
|
|
92
|
+
deepLinks: LinkTarget[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Starts a web-first link for a signed-in user (`POST /v1/identity/links`): Dewee returns a proof
|
|
96
|
+
* code the user sends from the chat account to link. The user never types an id; Dewee checks both
|
|
97
|
+
* sides and links on its own.
|
|
98
|
+
*/
|
|
99
|
+
export declare function startLink(opts: StartLinkOptions): Promise<StartedLink>;
|