sst 3.0.1-9 → 3.0.3
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/dist/auth/adapter/adapter.d.ts +24 -0
- package/dist/auth/adapter/adapter.js +4 -0
- package/dist/auth/adapter/apple.d.ts +5 -0
- package/dist/auth/adapter/apple.js +22 -0
- package/dist/auth/adapter/code.d.ts +8 -0
- package/dist/auth/adapter/code.js +49 -0
- package/dist/auth/adapter/facebook.d.ts +5 -0
- package/dist/auth/adapter/facebook.js +27 -0
- package/dist/auth/adapter/github.d.ts +12 -0
- package/dist/auth/adapter/github.js +23 -0
- package/dist/auth/adapter/google.d.ts +17 -0
- package/dist/auth/adapter/google.js +22 -0
- package/dist/auth/adapter/link.d.ts +6 -0
- package/dist/auth/adapter/link.js +27 -0
- package/dist/auth/adapter/microsoft.d.ts +11 -0
- package/dist/auth/adapter/microsoft.js +16 -0
- package/dist/auth/adapter/oauth.d.ts +33 -0
- package/dist/auth/adapter/oauth.js +79 -0
- package/dist/auth/adapter/oidc.d.ts +19 -0
- package/dist/auth/adapter/oidc.js +46 -0
- package/dist/auth/adapter/spotify.d.ts +12 -0
- package/dist/auth/adapter/spotify.js +22 -0
- package/dist/auth/example/bun.d.ts +2 -0
- package/dist/auth/example/bun.js +46 -0
- package/dist/auth/handler.d.ts +57 -0
- package/dist/auth/handler.js +189 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +13 -0
- package/dist/auth/session.d.ts +18 -0
- package/dist/auth/session.js +17 -0
- package/dist/resource.d.ts +1 -0
- package/dist/resource.js +19 -10
- package/dist/vector-client.d.ts +100 -40
- package/dist/vector-client.js +8 -8
- package/package.json +7 -3
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type { Context, Hono } from "hono";
|
|
3
|
+
import { KeyLike } from "jose";
|
|
4
|
+
export type Adapter<Properties = any> = (route: AdapterRoute, options: AdapterOptions<Properties>) => void;
|
|
5
|
+
export type AdapterRoute = Hono;
|
|
6
|
+
export interface AdapterOptions<Properties> {
|
|
7
|
+
name: string;
|
|
8
|
+
algorithm: string;
|
|
9
|
+
encryption: {
|
|
10
|
+
publicKey: Promise<KeyLike>;
|
|
11
|
+
privateKey: Promise<KeyLike>;
|
|
12
|
+
};
|
|
13
|
+
signing: {
|
|
14
|
+
publicKey: Promise<KeyLike>;
|
|
15
|
+
privateKey: Promise<KeyLike>;
|
|
16
|
+
};
|
|
17
|
+
success: (ctx: Context, properties: Properties) => Promise<Response>;
|
|
18
|
+
forward: (ctx: Context, response: Response) => Response;
|
|
19
|
+
cookie: (ctx: Context, key: string, value: string, maxAge: number) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare class AdapterError extends Error {
|
|
22
|
+
}
|
|
23
|
+
export declare class AdapterUnknownError extends AdapterError {
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { OauthBasicConfig } from "./oauth.js";
|
|
2
|
+
export declare const AppleAdapter: (config: OauthBasicConfig) => (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
3
|
+
tokenset: import("openid-client").TokenSet;
|
|
4
|
+
client: import("openid-client").BaseClient;
|
|
5
|
+
}>) => Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Issuer } from "openid-client";
|
|
2
|
+
import { OauthAdapter } from "./oauth.js";
|
|
3
|
+
// This adapter support the OAuth flow with the response_mode "form_post" for now.
|
|
4
|
+
// More details about the flow:
|
|
5
|
+
// https://developer.apple.com/documentation/devicemanagement/user_enrollment/onboarding_users_with_account_sign-in/implementing_the_oauth2_authentication_user-enrollment_flow
|
|
6
|
+
//
|
|
7
|
+
// Also note that Apple's discover uri does not work for the OAuth flow, as the
|
|
8
|
+
// userinfo_endpoint are not included in the response.
|
|
9
|
+
// await Issuer.discover("https://appleid.apple.com/.well-known/openid-configuration/");
|
|
10
|
+
const issuer = await Issuer.discover("https://appleid.apple.com/.well-known/openid-configuration");
|
|
11
|
+
export const AppleAdapter =
|
|
12
|
+
/* @__PURE__ */
|
|
13
|
+
(config) => {
|
|
14
|
+
return OauthAdapter({
|
|
15
|
+
issuer,
|
|
16
|
+
...config,
|
|
17
|
+
params: {
|
|
18
|
+
...config.params,
|
|
19
|
+
response_mode: "form_post",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
export declare function CodeAdapter(config: {
|
|
3
|
+
length?: number;
|
|
4
|
+
onCodeRequest: (code: string, claims: Record<string, any>, req: Request) => Promise<Response>;
|
|
5
|
+
onCodeInvalid: (code: string, claims: Record<string, any>, req: Request) => Promise<Response>;
|
|
6
|
+
}): (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
7
|
+
claims: Record<string, string>;
|
|
8
|
+
}>) => void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
import { deleteCookie, getCookie } from "hono/cookie";
|
|
3
|
+
import { UnknownStateError } from "../index.js";
|
|
4
|
+
export function CodeAdapter(config) {
|
|
5
|
+
const length = config.length || 6;
|
|
6
|
+
function generate() {
|
|
7
|
+
const buffer = crypto.getRandomValues(new Uint8Array(length));
|
|
8
|
+
const otp = Array.from(buffer)
|
|
9
|
+
.map((byte) => byte % 10)
|
|
10
|
+
.join("");
|
|
11
|
+
return otp;
|
|
12
|
+
}
|
|
13
|
+
return function (routes, ctx) {
|
|
14
|
+
routes.get("/authorize", async (c) => {
|
|
15
|
+
const code = generate();
|
|
16
|
+
const claims = c.req.query();
|
|
17
|
+
delete claims["client_id"];
|
|
18
|
+
delete claims["redirect_uri"];
|
|
19
|
+
delete claims["response_type"];
|
|
20
|
+
delete claims["provider"];
|
|
21
|
+
const authorization = await new jose.CompactEncrypt(new TextEncoder().encode(JSON.stringify({
|
|
22
|
+
claims,
|
|
23
|
+
code,
|
|
24
|
+
})))
|
|
25
|
+
.setProtectedHeader({ alg: "RSA-OAEP-512", enc: "A256GCM" })
|
|
26
|
+
.encrypt(await ctx.encryption.publicKey);
|
|
27
|
+
ctx.cookie(c, "authorization", authorization, 60 * 10);
|
|
28
|
+
return ctx.forward(c, await config.onCodeRequest(code, claims, c.req.raw));
|
|
29
|
+
});
|
|
30
|
+
routes.get("/callback", async (c) => {
|
|
31
|
+
const authorization = getCookie(c, "authorization");
|
|
32
|
+
if (!authorization)
|
|
33
|
+
throw new UnknownStateError();
|
|
34
|
+
const { code, claims } = JSON.parse(new TextDecoder().decode(await jose
|
|
35
|
+
.compactDecrypt(authorization, await ctx.encryption.privateKey)
|
|
36
|
+
.then((value) => value.plaintext)));
|
|
37
|
+
if (!code || !claims) {
|
|
38
|
+
return ctx.forward(c, await config.onCodeInvalid(code, claims, c.req.raw));
|
|
39
|
+
}
|
|
40
|
+
const compare = c.req.query("code");
|
|
41
|
+
console.log("comparing", code, "to", compare);
|
|
42
|
+
if (code !== compare) {
|
|
43
|
+
return ctx.forward(c, await config.onCodeInvalid(code, claims, c.req.raw));
|
|
44
|
+
}
|
|
45
|
+
deleteCookie(c, "authorization");
|
|
46
|
+
return ctx.forward(c, await ctx.success(c, { claims }));
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { OauthBasicConfig } from "./oauth.js";
|
|
2
|
+
export declare const FacebookAdapter: (config: OauthBasicConfig) => (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
3
|
+
tokenset: import("openid-client").TokenSet;
|
|
4
|
+
client: import("openid-client").BaseClient;
|
|
5
|
+
}>) => Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Issuer } from "openid-client";
|
|
2
|
+
import { OauthAdapter } from "./oauth.js";
|
|
3
|
+
// Facebook's OIDC flow returns "id_token" as uri hash in redirect uri. Hashes
|
|
4
|
+
// are not passed to Lambda event object. It is likely that Facebook only wants
|
|
5
|
+
// to support redirecting to a frontend uri.
|
|
6
|
+
//
|
|
7
|
+
// We are only going to support the OAuth flow for now. More details about the flow:
|
|
8
|
+
// https://developers.facebook.com/docs/facebook-login/guides/advanced/oidc-token
|
|
9
|
+
//
|
|
10
|
+
// Also note that Facebook's discover uri does not work for the OAuth flow, as the
|
|
11
|
+
// token_endpoint and userinfo_endpoint are not included in the response.
|
|
12
|
+
// await Issuer.discover("https://www.facebook.com/.well-known/openid-configuration/");
|
|
13
|
+
const issuer = new Issuer({
|
|
14
|
+
issuer: "https://www.facebook.com",
|
|
15
|
+
authorization_endpoint: "https://facebook.com/dialog/oauth/",
|
|
16
|
+
jwks_uri: "https://www.facebook.com/.well-known/oauth/openid/jwks/",
|
|
17
|
+
token_endpoint: "https://graph.facebook.com/oauth/access_token",
|
|
18
|
+
userinfo_endpoint: "https://graph.facebook.com/oauth/access_token",
|
|
19
|
+
});
|
|
20
|
+
export const FacebookAdapter =
|
|
21
|
+
/* @__PURE__ */
|
|
22
|
+
(config) => {
|
|
23
|
+
return OauthAdapter({
|
|
24
|
+
issuer,
|
|
25
|
+
...config,
|
|
26
|
+
});
|
|
27
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OauthBasicConfig } from "./oauth.js";
|
|
2
|
+
import { OidcBasicConfig } from "./oidc.js";
|
|
3
|
+
type Config = ({
|
|
4
|
+
mode: "oauth";
|
|
5
|
+
} & OauthBasicConfig) | ({
|
|
6
|
+
mode: "oidc";
|
|
7
|
+
} & OidcBasicConfig);
|
|
8
|
+
export declare const GithubAdapter: (config: Config) => (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
9
|
+
tokenset: import("openid-client").TokenSet;
|
|
10
|
+
client: import("openid-client").BaseClient;
|
|
11
|
+
}>) => Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Issuer } from "openid-client";
|
|
2
|
+
import { OauthAdapter } from "./oauth.js";
|
|
3
|
+
import { OidcAdapter } from "./oidc.js";
|
|
4
|
+
const issuer = new Issuer({
|
|
5
|
+
issuer: "https://github.com",
|
|
6
|
+
authorization_endpoint: "https://github.com/login/oauth/authorize",
|
|
7
|
+
token_endpoint: "https://github.com/login/oauth/access_token",
|
|
8
|
+
});
|
|
9
|
+
export const GithubAdapter =
|
|
10
|
+
/* @__PURE__ */
|
|
11
|
+
(config) => {
|
|
12
|
+
if (config.mode === "oauth") {
|
|
13
|
+
return OauthAdapter({
|
|
14
|
+
issuer,
|
|
15
|
+
...config,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return OidcAdapter({
|
|
19
|
+
issuer,
|
|
20
|
+
scope: "openid email profile",
|
|
21
|
+
...config,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OidcBasicConfig } from "./oidc.js";
|
|
2
|
+
import { OauthBasicConfig } from "./oauth.js";
|
|
3
|
+
type GooglePrompt = "none" | "consent" | "select_account";
|
|
4
|
+
type GoogleAccessType = "offline" | "online";
|
|
5
|
+
type GoogleConfig = (OauthBasicConfig & {
|
|
6
|
+
mode: "oauth";
|
|
7
|
+
prompt?: GooglePrompt;
|
|
8
|
+
accessType?: GoogleAccessType;
|
|
9
|
+
}) | (OidcBasicConfig & {
|
|
10
|
+
mode: "oidc";
|
|
11
|
+
prompt?: GooglePrompt;
|
|
12
|
+
});
|
|
13
|
+
export declare function GoogleAdapter(config: GoogleConfig): (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
14
|
+
tokenset: import("openid-client").TokenSet;
|
|
15
|
+
client: import("openid-client").BaseClient;
|
|
16
|
+
}>) => Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Issuer } from "openid-client";
|
|
2
|
+
import { OidcAdapter } from "./oidc.js";
|
|
3
|
+
import { OauthAdapter } from "./oauth.js";
|
|
4
|
+
const issuer = await Issuer.discover("https://accounts.google.com");
|
|
5
|
+
export function GoogleAdapter(config) {
|
|
6
|
+
/* @__PURE__ */
|
|
7
|
+
if (config.mode === "oauth") {
|
|
8
|
+
return OauthAdapter({
|
|
9
|
+
issuer,
|
|
10
|
+
...config,
|
|
11
|
+
params: {
|
|
12
|
+
...(config.accessType && { access_type: config.accessType }),
|
|
13
|
+
...config.params,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return OidcAdapter({
|
|
18
|
+
issuer,
|
|
19
|
+
scope: "openid email profile",
|
|
20
|
+
...config,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
export declare function LinkAdapter(config: {
|
|
3
|
+
onLink: (link: string, claims: Record<string, any>) => Promise<Response>;
|
|
4
|
+
}): (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
5
|
+
claims: Record<string, string>;
|
|
6
|
+
}>) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
export function LinkAdapter(config) {
|
|
3
|
+
return function (routes, ctx) {
|
|
4
|
+
routes.get("/authorize", async (c) => {
|
|
5
|
+
const token = await new jose.SignJWT(c.req.query())
|
|
6
|
+
.setProtectedHeader({ alg: ctx.algorithm })
|
|
7
|
+
.setExpirationTime("10m")
|
|
8
|
+
.sign(await ctx.signing.privateKey);
|
|
9
|
+
const url = new URL(new URL(c.req.url).origin);
|
|
10
|
+
url.pathname = `/${ctx.name}/callback`;
|
|
11
|
+
for (const key of url.searchParams.keys()) {
|
|
12
|
+
url.searchParams.delete(key);
|
|
13
|
+
}
|
|
14
|
+
url.searchParams.set("token", token);
|
|
15
|
+
const resp = ctx.forward(c, await config.onLink(url.toString(), c.req.query()));
|
|
16
|
+
return resp;
|
|
17
|
+
});
|
|
18
|
+
routes.get("/callback", async (c) => {
|
|
19
|
+
const token = c.req.query("token");
|
|
20
|
+
if (!token)
|
|
21
|
+
throw new Error("Missing token parameter");
|
|
22
|
+
const verified = await jose.jwtVerify(token, await ctx.signing.publicKey);
|
|
23
|
+
const resp = await ctx.success(c, { claims: verified.payload });
|
|
24
|
+
return resp;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OidcBasicConfig } from "./oidc.js";
|
|
2
|
+
type MicrosoftConfig = OidcBasicConfig & {
|
|
3
|
+
mode: "oidc";
|
|
4
|
+
prompt?: "login" | "none" | "consent" | "select_account";
|
|
5
|
+
tenantID?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function MicrosoftAdapter(config: MicrosoftConfig): (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
8
|
+
tokenset: import("openid-client").TokenSet;
|
|
9
|
+
client: import("openid-client").BaseClient;
|
|
10
|
+
}>) => Promise<never>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Issuer } from "openid-client";
|
|
2
|
+
import { OidcAdapter } from "./oidc.js";
|
|
3
|
+
export function MicrosoftAdapter(config) {
|
|
4
|
+
const authority = config?.tenantID ?? "common";
|
|
5
|
+
const issuer = `https://login.microsoftonline.com/${authority}`;
|
|
6
|
+
return OidcAdapter({
|
|
7
|
+
issuer: new Issuer({
|
|
8
|
+
issuer: `${issuer}/v2.0`,
|
|
9
|
+
authorization_endpoint: `${issuer}/oauth2/v2.0/authorize`,
|
|
10
|
+
token_endpoint: `${issuer}/oauth2/v2.0/token`,
|
|
11
|
+
jwks_uri: `${issuer}/discovery/v2.0/keys`,
|
|
12
|
+
}),
|
|
13
|
+
scope: "openid email profile",
|
|
14
|
+
...config,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BaseClient, Issuer, TokenSet } from "openid-client";
|
|
2
|
+
import { AdapterError } from "./adapter.js";
|
|
3
|
+
export interface OauthBasicConfig {
|
|
4
|
+
/**
|
|
5
|
+
* The clientID provided by the third party oauth service
|
|
6
|
+
*/
|
|
7
|
+
clientID: string;
|
|
8
|
+
/**
|
|
9
|
+
* The clientSecret provided by the third party oauth service
|
|
10
|
+
*/
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
/**
|
|
13
|
+
* Various scopes requested for the access token
|
|
14
|
+
*/
|
|
15
|
+
scope: string;
|
|
16
|
+
/**
|
|
17
|
+
* Determines whether users will be prompted for reauthentication and consent
|
|
18
|
+
*/
|
|
19
|
+
prompt?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Additional parameters to be passed to the authorization endpoint
|
|
22
|
+
*/
|
|
23
|
+
params?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
export interface OauthConfig extends OauthBasicConfig {
|
|
26
|
+
issuer: Issuer;
|
|
27
|
+
}
|
|
28
|
+
export declare class OauthError extends AdapterError {
|
|
29
|
+
}
|
|
30
|
+
export declare const OauthAdapter: (config: OauthConfig) => (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
31
|
+
tokenset: TokenSet;
|
|
32
|
+
client: BaseClient;
|
|
33
|
+
}>) => Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { generators } from "openid-client";
|
|
2
|
+
import { AdapterError } from "./adapter.js";
|
|
3
|
+
import { getCookie } from "hono/cookie";
|
|
4
|
+
export class OauthError extends AdapterError {
|
|
5
|
+
}
|
|
6
|
+
export const OauthAdapter =
|
|
7
|
+
/* @__PURE__ */
|
|
8
|
+
(config) => {
|
|
9
|
+
return async function (routes, ctx) {
|
|
10
|
+
function getClient(c) {
|
|
11
|
+
const callback = c.req.url.replace(/authorize$/, "callback");
|
|
12
|
+
return [
|
|
13
|
+
callback,
|
|
14
|
+
new config.issuer.Client({
|
|
15
|
+
client_id: config.clientID,
|
|
16
|
+
client_secret: config.clientSecret,
|
|
17
|
+
redirect_uris: [callback],
|
|
18
|
+
response_types: ["code"],
|
|
19
|
+
}),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
routes.get("/authorize", async (c) => {
|
|
23
|
+
const [_, client] = getClient(c);
|
|
24
|
+
const code_verifier = generators.codeVerifier();
|
|
25
|
+
const state = generators.state();
|
|
26
|
+
const code_challenge = generators.codeChallenge(code_verifier);
|
|
27
|
+
const url = client.authorizationUrl({
|
|
28
|
+
scope: config.scope,
|
|
29
|
+
code_challenge: code_challenge,
|
|
30
|
+
code_challenge_method: "S256",
|
|
31
|
+
state,
|
|
32
|
+
prompt: config.prompt,
|
|
33
|
+
...config.params,
|
|
34
|
+
});
|
|
35
|
+
ctx.cookie(c, "auth_code_verifier", code_verifier, 60 * 10);
|
|
36
|
+
ctx.cookie(c, "auth_state", state, 60 * 10);
|
|
37
|
+
return c.redirect(url);
|
|
38
|
+
});
|
|
39
|
+
routes.get("/callback", async (c) => {
|
|
40
|
+
const [callback, client] = getClient(c);
|
|
41
|
+
const query = c.req.query();
|
|
42
|
+
if (query.error) {
|
|
43
|
+
throw new OauthError(query.error);
|
|
44
|
+
}
|
|
45
|
+
const code_verifier = getCookie(c, "auth_code_verifier");
|
|
46
|
+
const state = getCookie(c, "auth_state");
|
|
47
|
+
const tokenset = await client[config.issuer.metadata.userinfo_endpoint
|
|
48
|
+
? "callback"
|
|
49
|
+
: "oauthCallback"](callback, query, {
|
|
50
|
+
code_verifier,
|
|
51
|
+
state,
|
|
52
|
+
});
|
|
53
|
+
return ctx.success(c, {
|
|
54
|
+
client,
|
|
55
|
+
tokenset,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
// response_mode=form_post
|
|
59
|
+
routes.get("/callback", async (c) => {
|
|
60
|
+
const [callback, client] = getClient(c);
|
|
61
|
+
const form = await c.req.formData();
|
|
62
|
+
if (form.get("error")) {
|
|
63
|
+
throw new OauthError(form.get("error").toString());
|
|
64
|
+
}
|
|
65
|
+
const code_verifier = getCookie(c, "auth_code_verifier");
|
|
66
|
+
const state = getCookie(c, "auth_state");
|
|
67
|
+
const tokenset = await client[config.issuer.metadata.userinfo_endpoint
|
|
68
|
+
? "callback"
|
|
69
|
+
: "oauthCallback"](callback, Object.fromEntries(form), {
|
|
70
|
+
code_verifier,
|
|
71
|
+
state,
|
|
72
|
+
});
|
|
73
|
+
return ctx.success(c, {
|
|
74
|
+
client,
|
|
75
|
+
tokenset,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseClient, Issuer, TokenSet } from "openid-client";
|
|
2
|
+
export interface OidcBasicConfig {
|
|
3
|
+
/**
|
|
4
|
+
* The clientID provided by the third party oauth service
|
|
5
|
+
*/
|
|
6
|
+
clientID: string;
|
|
7
|
+
/**
|
|
8
|
+
* Determines whether users will be prompted for reauthentication and consent
|
|
9
|
+
*/
|
|
10
|
+
prompt?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface OidcConfig extends OidcBasicConfig {
|
|
13
|
+
issuer: Issuer;
|
|
14
|
+
scope: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const OidcAdapter: (config: OidcConfig) => (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
17
|
+
tokenset: TokenSet;
|
|
18
|
+
client: BaseClient;
|
|
19
|
+
}>) => Promise<never>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { generators } from "openid-client";
|
|
2
|
+
import { getCookie } from "hono/cookie";
|
|
3
|
+
export const OidcAdapter = /* @__PURE__ */ (config) => {
|
|
4
|
+
return async function (routes, ctx) {
|
|
5
|
+
routes.get("/authorize", async (c) => {
|
|
6
|
+
const callback = c.req.url.replace(/authorize$/, "callback");
|
|
7
|
+
const client = new config.issuer.Client({
|
|
8
|
+
client_id: config.clientID,
|
|
9
|
+
redirect_uris: [callback],
|
|
10
|
+
response_types: ["id_token"],
|
|
11
|
+
});
|
|
12
|
+
const nonce = generators.nonce();
|
|
13
|
+
const state = generators.state();
|
|
14
|
+
const url = client.authorizationUrl({
|
|
15
|
+
scope: config.scope,
|
|
16
|
+
response_mode: "form_post",
|
|
17
|
+
nonce,
|
|
18
|
+
state,
|
|
19
|
+
prompt: config.prompt,
|
|
20
|
+
});
|
|
21
|
+
ctx.cookie(c, "auth_nonce", nonce, 60 * 10);
|
|
22
|
+
ctx.cookie(c, "auth_state", state, 60 * 10);
|
|
23
|
+
return c.redirect(url);
|
|
24
|
+
});
|
|
25
|
+
routes.post("/callback", async (c) => {
|
|
26
|
+
const callback = c.req.url.replace(/authorize$/, "callback");
|
|
27
|
+
const client = new config.issuer.Client({
|
|
28
|
+
client_id: config.clientID,
|
|
29
|
+
redirect_uris: [callback],
|
|
30
|
+
response_types: ["id_token"],
|
|
31
|
+
});
|
|
32
|
+
const form = await c.req.formData();
|
|
33
|
+
const nonce = getCookie(c, "auth_nonce");
|
|
34
|
+
const state = getCookie(c, "auth_state");
|
|
35
|
+
const tokenset = await client.callback(callback, Object.fromEntries(form), {
|
|
36
|
+
nonce,
|
|
37
|
+
state,
|
|
38
|
+
});
|
|
39
|
+
return ctx.success(c, {
|
|
40
|
+
tokenset,
|
|
41
|
+
client,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
throw new Error("Invalid auth request");
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OauthBasicConfig } from "./oauth.js";
|
|
2
|
+
/**
|
|
3
|
+
* The Spotify Adapter follows the PKCE flow outlined here:
|
|
4
|
+
* https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow
|
|
5
|
+
*
|
|
6
|
+
* List of scopes available:
|
|
7
|
+
* https://developer.spotify.com/documentation/web-api/concepts/scopes
|
|
8
|
+
*/
|
|
9
|
+
export declare const SpotifyAdapter: (config: OauthBasicConfig) => (routes: import("./adapter.js").AdapterRoute, ctx: import("./adapter.js").AdapterOptions<{
|
|
10
|
+
tokenset: import("openid-client").TokenSet;
|
|
11
|
+
client: import("openid-client").BaseClient;
|
|
12
|
+
}>) => Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Issuer } from "openid-client";
|
|
2
|
+
import { OauthAdapter } from "./oauth.js";
|
|
3
|
+
const issuer = new Issuer({
|
|
4
|
+
issuer: "https://accounts.spotify.com",
|
|
5
|
+
authorization_endpoint: "https://accounts.spotify.com/authorize",
|
|
6
|
+
token_endpoint: "https://accounts.spotify.com/api/token",
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* The Spotify Adapter follows the PKCE flow outlined here:
|
|
10
|
+
* https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow
|
|
11
|
+
*
|
|
12
|
+
* List of scopes available:
|
|
13
|
+
* https://developer.spotify.com/documentation/web-api/concepts/scopes
|
|
14
|
+
*/
|
|
15
|
+
export const SpotifyAdapter =
|
|
16
|
+
/* @__PURE__ */
|
|
17
|
+
(config) => {
|
|
18
|
+
return OauthAdapter({
|
|
19
|
+
issuer,
|
|
20
|
+
...config,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AuthHandler } from "../handler.js";
|
|
2
|
+
import { LinkAdapter } from "../adapter/link.js";
|
|
3
|
+
import { createSessionBuilder } from "../session.js";
|
|
4
|
+
import { CodeAdapter } from "../index.js";
|
|
5
|
+
const sessions = createSessionBuilder();
|
|
6
|
+
export default AuthHandler({
|
|
7
|
+
providers: {
|
|
8
|
+
link: LinkAdapter({
|
|
9
|
+
async onLink(link, claims) {
|
|
10
|
+
return new Response(link, {
|
|
11
|
+
status: 200,
|
|
12
|
+
headers: { "Content-Type": "text/plain" },
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
code: CodeAdapter({
|
|
17
|
+
onCodeRequest: async (code, claims) => {
|
|
18
|
+
return new Response("Your code is " + code, {
|
|
19
|
+
status: 200,
|
|
20
|
+
headers: { "Content-Type": "text/plain" },
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
onCodeInvalid: async (code, claims) => {
|
|
24
|
+
return new Response("Code is invalid " + code, {
|
|
25
|
+
status: 200,
|
|
26
|
+
headers: { "Content-Type": "text/plain" },
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
callbacks: {
|
|
32
|
+
auth: {
|
|
33
|
+
async allowClient(input) {
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
async success(ctx, input) {
|
|
37
|
+
return ctx.session({
|
|
38
|
+
type: "user",
|
|
39
|
+
properties: {
|
|
40
|
+
email: input.claims.email,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { Adapter } from "./adapter/adapter.js";
|
|
3
|
+
import * as jose from "jose";
|
|
4
|
+
import { SessionBuilder } from "./session.js";
|
|
5
|
+
import { Hono } from "hono/tiny";
|
|
6
|
+
interface OnSuccessResponder<T extends {
|
|
7
|
+
type: any;
|
|
8
|
+
properties: any;
|
|
9
|
+
}> {
|
|
10
|
+
session(input: T & jose.JWTPayload): Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
export declare class UnknownProviderError extends Error {
|
|
13
|
+
provider?: string | undefined;
|
|
14
|
+
constructor(provider?: string | undefined);
|
|
15
|
+
}
|
|
16
|
+
export declare class MissingParameterError extends Error {
|
|
17
|
+
parameter: string;
|
|
18
|
+
constructor(parameter: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class UnknownStateError extends Error {
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
23
|
+
export declare class UnauthorizedClientError extends Error {
|
|
24
|
+
client: string;
|
|
25
|
+
redirect_uri: string;
|
|
26
|
+
constructor(client: string, redirect_uri: string);
|
|
27
|
+
}
|
|
28
|
+
export declare class InvalidSessionError extends Error {
|
|
29
|
+
constructor();
|
|
30
|
+
}
|
|
31
|
+
export type Prettify<T> = {
|
|
32
|
+
[K in keyof T]: T[K];
|
|
33
|
+
} & {};
|
|
34
|
+
export declare function AuthHandler<Providers extends Record<string, Adapter<any>>, Sessions extends SessionBuilder = SessionBuilder, Result = {
|
|
35
|
+
[key in keyof Providers]: Prettify<{
|
|
36
|
+
provider: key;
|
|
37
|
+
} & (Providers[key] extends Adapter<infer T> ? T : {})>;
|
|
38
|
+
}[keyof Providers]>(input: {
|
|
39
|
+
session?: Sessions;
|
|
40
|
+
providers: Providers;
|
|
41
|
+
callbacks: {
|
|
42
|
+
index?(req: Request): Promise<Response>;
|
|
43
|
+
error?(error: UnknownStateError, req: Request): Promise<Response | undefined>;
|
|
44
|
+
auth: {
|
|
45
|
+
error?(error: MissingParameterError | UnauthorizedClientError | UnknownProviderError, req: Request): Promise<Response>;
|
|
46
|
+
start?(event: Request): Promise<void>;
|
|
47
|
+
allowClient(clientID: string, redirect: string, req: Request): Promise<boolean>;
|
|
48
|
+
success(response: OnSuccessResponder<Sessions["$typeValues"]>, input: Result, req: Request): Promise<Response>;
|
|
49
|
+
};
|
|
50
|
+
connect?: {
|
|
51
|
+
error?(error: InvalidSessionError | UnknownProviderError, req: Request): Promise<Response | undefined>;
|
|
52
|
+
start?(session: Sessions["$typeValues"], req: Request): Promise<void>;
|
|
53
|
+
success?(session: Sessions["$typeValues"], input: {}): Promise<Response>;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}): Hono<import("hono").Env, import("hono/types").BlankSchema, "/">;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
import { Hono } from "hono/tiny";
|
|
3
|
+
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
4
|
+
export class UnknownProviderError extends Error {
|
|
5
|
+
provider;
|
|
6
|
+
constructor(provider) {
|
|
7
|
+
super("Unknown provider: " + provider);
|
|
8
|
+
this.provider = provider;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class MissingParameterError extends Error {
|
|
12
|
+
parameter;
|
|
13
|
+
constructor(parameter) {
|
|
14
|
+
super("Missing parameter: " + parameter);
|
|
15
|
+
this.parameter = parameter;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class UnknownStateError extends Error {
|
|
19
|
+
constructor() {
|
|
20
|
+
super("The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class UnauthorizedClientError extends Error {
|
|
24
|
+
client;
|
|
25
|
+
redirect_uri;
|
|
26
|
+
constructor(client, redirect_uri) {
|
|
27
|
+
super("Unauthorized client");
|
|
28
|
+
this.client = client;
|
|
29
|
+
this.redirect_uri = redirect_uri;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class InvalidSessionError extends Error {
|
|
33
|
+
constructor() {
|
|
34
|
+
super("Invalid session");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function AuthHandler(input) {
|
|
38
|
+
const app = new Hono();
|
|
39
|
+
if (!input.callbacks.auth.error) {
|
|
40
|
+
input.callbacks.auth.error = async (err) => {
|
|
41
|
+
return new Response(err.message, {
|
|
42
|
+
status: 400,
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "text/plain",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const options = {
|
|
50
|
+
signing: {
|
|
51
|
+
privateKey: jose.importPKCS8(process.env.AUTH_PRIVATE_KEY, "RS512"),
|
|
52
|
+
publicKey: jose.importSPKI(process.env.AUTH_PUBLIC_KEY, "RS512"),
|
|
53
|
+
},
|
|
54
|
+
encryption: {
|
|
55
|
+
privateKey: jose.importPKCS8(process.env.AUTH_PRIVATE_KEY, "RSA-OAEP-512"),
|
|
56
|
+
publicKey: jose.importSPKI(process.env.AUTH_PUBLIC_KEY, "RSA-OAEP-512"),
|
|
57
|
+
},
|
|
58
|
+
algorithm: "RS512",
|
|
59
|
+
async success(ctx, properties) {
|
|
60
|
+
const redirect_uri = getCookie(ctx, "redirect_uri");
|
|
61
|
+
const response_type = getCookie(ctx, "response_type");
|
|
62
|
+
if (!redirect_uri) {
|
|
63
|
+
return options.forward(ctx, await input.callbacks.auth.error(new UnknownStateError(), ctx.req.raw));
|
|
64
|
+
}
|
|
65
|
+
return await input.callbacks.auth.success({
|
|
66
|
+
async session(session) {
|
|
67
|
+
const token = await new jose.SignJWT(session)
|
|
68
|
+
.setProtectedHeader({ alg: "RS512" })
|
|
69
|
+
.setExpirationTime("1yr")
|
|
70
|
+
.sign(await options.signing.privateKey);
|
|
71
|
+
deleteCookie(ctx, "provider");
|
|
72
|
+
deleteCookie(ctx, "response_type");
|
|
73
|
+
deleteCookie(ctx, "redirect_uri");
|
|
74
|
+
deleteCookie(ctx, "state");
|
|
75
|
+
const client_id = getCookie(ctx, "client_id");
|
|
76
|
+
const state = getCookie(ctx, "state");
|
|
77
|
+
if (response_type === "token") {
|
|
78
|
+
const location = new URL(redirect_uri);
|
|
79
|
+
location.hash = `access_token=${token}&state=${state || ""}`;
|
|
80
|
+
return ctx.redirect(location.toString(), 302);
|
|
81
|
+
}
|
|
82
|
+
if (response_type === "code") {
|
|
83
|
+
// This allows the code to be reused within a 30 second window
|
|
84
|
+
// The code should be single use but we're making this tradeoff to remain stateless
|
|
85
|
+
// In the future can store this in a dynamo table to ensure single use
|
|
86
|
+
const code = await new jose.SignJWT({
|
|
87
|
+
client_id,
|
|
88
|
+
redirect_uri,
|
|
89
|
+
token,
|
|
90
|
+
})
|
|
91
|
+
.setProtectedHeader({ alg: "RS512" })
|
|
92
|
+
.setExpirationTime("30s")
|
|
93
|
+
.sign(await options.signing.privateKey);
|
|
94
|
+
const location = new URL(redirect_uri);
|
|
95
|
+
location.searchParams.set("code", code);
|
|
96
|
+
location.searchParams.set("state", state || "");
|
|
97
|
+
return ctx.redirect(location.toString(), 302);
|
|
98
|
+
}
|
|
99
|
+
ctx.status(400);
|
|
100
|
+
return ctx.text(`Unsupported response_type: ${response_type}`);
|
|
101
|
+
},
|
|
102
|
+
}, {
|
|
103
|
+
provider: ctx.get("provider"),
|
|
104
|
+
...properties,
|
|
105
|
+
}, ctx.req.raw);
|
|
106
|
+
},
|
|
107
|
+
forward(ctx, response) {
|
|
108
|
+
return ctx.newResponse(response.body, response.status, Object.fromEntries(response.headers.entries()));
|
|
109
|
+
},
|
|
110
|
+
cookie(c, key, value, maxAge) {
|
|
111
|
+
setCookie(c, key, value, {
|
|
112
|
+
maxAge,
|
|
113
|
+
httpOnly: true,
|
|
114
|
+
...(c.req.url.startsWith("https://")
|
|
115
|
+
? { secure: true, sameSite: "None" }
|
|
116
|
+
: {}),
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
app.get("/token", async (c) => {
|
|
121
|
+
console.log("token request");
|
|
122
|
+
const form = await c.req.formData();
|
|
123
|
+
if (form.get("grant_type") !== "authorization_code") {
|
|
124
|
+
c.status(400);
|
|
125
|
+
return c.text("Invalid grant_type");
|
|
126
|
+
}
|
|
127
|
+
const code = form.get("code");
|
|
128
|
+
if (!code) {
|
|
129
|
+
c.status(400);
|
|
130
|
+
return c.text("Missing code");
|
|
131
|
+
}
|
|
132
|
+
const { payload } = await jose.jwtVerify(code, await options.signing.publicKey);
|
|
133
|
+
if (payload.redirect_uri !== form.get("redirect_uri")) {
|
|
134
|
+
c.status(400);
|
|
135
|
+
return c.text("redirect_uri mismatch");
|
|
136
|
+
}
|
|
137
|
+
if (payload.client_id !== form.get("client_id")) {
|
|
138
|
+
c.status(400);
|
|
139
|
+
return c.text("client_id mismatch");
|
|
140
|
+
}
|
|
141
|
+
return c.json({
|
|
142
|
+
access_token: payload.token,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
app.use("/:provider/authorize", async (c, next) => {
|
|
146
|
+
const provider = c.req.param("provider");
|
|
147
|
+
console.log("authorize request for", provider);
|
|
148
|
+
const response_type = c.req.query("response_type") || getCookie(c, "response_type");
|
|
149
|
+
const redirect_uri = c.req.query("redirect_uri") || getCookie(c, "redirect_uri");
|
|
150
|
+
const state = c.req.query("state") || getCookie(c, "state");
|
|
151
|
+
if (!provider) {
|
|
152
|
+
c.status(400);
|
|
153
|
+
return c.text("Missing provider");
|
|
154
|
+
}
|
|
155
|
+
if (!redirect_uri) {
|
|
156
|
+
c.status(400);
|
|
157
|
+
return c.text("Missing redirect_uri");
|
|
158
|
+
}
|
|
159
|
+
if (!response_type) {
|
|
160
|
+
c.status(400);
|
|
161
|
+
return c.text("Missing response_type");
|
|
162
|
+
}
|
|
163
|
+
options.cookie(c, "provider", provider, 60 * 10);
|
|
164
|
+
options.cookie(c, "response_type", response_type, 60 * 10);
|
|
165
|
+
options.cookie(c, "redirect_uri", redirect_uri, 60 * 10);
|
|
166
|
+
options.cookie(c, "state", state || "", 60 * 10);
|
|
167
|
+
if (input.callbacks.auth.start) {
|
|
168
|
+
await input.callbacks.auth.start(c.req.raw);
|
|
169
|
+
}
|
|
170
|
+
await next();
|
|
171
|
+
});
|
|
172
|
+
for (const [name, value] of Object.entries(input.providers)) {
|
|
173
|
+
const route = new Hono();
|
|
174
|
+
route.use(async (c, next) => {
|
|
175
|
+
c.set("provider", name);
|
|
176
|
+
await next();
|
|
177
|
+
});
|
|
178
|
+
value(route, {
|
|
179
|
+
name,
|
|
180
|
+
...options,
|
|
181
|
+
});
|
|
182
|
+
app.route(`/${name}`, route);
|
|
183
|
+
}
|
|
184
|
+
app.all("/*", async (c) => {
|
|
185
|
+
return c.notFound();
|
|
186
|
+
});
|
|
187
|
+
console.log(app.routes);
|
|
188
|
+
return app;
|
|
189
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./adapter/oidc.js";
|
|
2
|
+
export * from "./adapter/google.js";
|
|
3
|
+
export * from "./adapter/link.js";
|
|
4
|
+
export * from "./adapter/github.js";
|
|
5
|
+
export * from "./adapter/facebook.js";
|
|
6
|
+
export * from "./adapter/microsoft.js";
|
|
7
|
+
export * from "./adapter/oauth.js";
|
|
8
|
+
export * from "./adapter/spotify.js";
|
|
9
|
+
export * from "./adapter/code.js";
|
|
10
|
+
export * from "./adapter/apple.js";
|
|
11
|
+
export type { Adapter } from "./adapter/adapter.js";
|
|
12
|
+
export * from "./session.js";
|
|
13
|
+
export * from "./handler.js";
|
|
14
|
+
export { Issuer } from "openid-client";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./adapter/oidc.js";
|
|
2
|
+
export * from "./adapter/google.js";
|
|
3
|
+
export * from "./adapter/link.js";
|
|
4
|
+
export * from "./adapter/github.js";
|
|
5
|
+
export * from "./adapter/facebook.js";
|
|
6
|
+
export * from "./adapter/microsoft.js";
|
|
7
|
+
export * from "./adapter/oauth.js";
|
|
8
|
+
export * from "./adapter/spotify.js";
|
|
9
|
+
export * from "./adapter/code.js";
|
|
10
|
+
export * from "./adapter/apple.js";
|
|
11
|
+
export * from "./session.js";
|
|
12
|
+
export * from "./handler.js";
|
|
13
|
+
export { Issuer } from "openid-client";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type SessionBuilder = ReturnType<typeof createSessionBuilder>;
|
|
2
|
+
export declare function createSessionBuilder<SessionTypes extends Record<string, any> = {}>(): {
|
|
3
|
+
verify(token: string): Promise<{ [type in keyof SessionTypes]: {
|
|
4
|
+
type: type;
|
|
5
|
+
properties: SessionTypes[type];
|
|
6
|
+
}; }[keyof SessionTypes] | {
|
|
7
|
+
type: "public";
|
|
8
|
+
properties: {};
|
|
9
|
+
}>;
|
|
10
|
+
$type: SessionTypes;
|
|
11
|
+
$typeValues: { [type in keyof SessionTypes]: {
|
|
12
|
+
type: type;
|
|
13
|
+
properties: SessionTypes[type];
|
|
14
|
+
}; }[keyof SessionTypes] | {
|
|
15
|
+
type: "public";
|
|
16
|
+
properties: {};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { importSPKI, jwtVerify } from "jose";
|
|
2
|
+
import { Resource } from "../resource.js";
|
|
3
|
+
export function createSessionBuilder() {
|
|
4
|
+
return {
|
|
5
|
+
async verify(token) {
|
|
6
|
+
const auth = Object.values(Resource).find((value) => value.publicKey);
|
|
7
|
+
if (!auth) {
|
|
8
|
+
throw new Error("No auth resource found");
|
|
9
|
+
}
|
|
10
|
+
const publicKey = auth.publicKey;
|
|
11
|
+
const result = await jwtVerify(token, await importSPKI(publicKey, "RS512"));
|
|
12
|
+
return result.payload;
|
|
13
|
+
},
|
|
14
|
+
$type: {},
|
|
15
|
+
$typeValues: {},
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/resource.d.ts
CHANGED
package/dist/resource.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { env } from "node:process";
|
|
2
|
+
const raw = {
|
|
3
|
+
// @ts-expect-error,
|
|
4
|
+
...globalThis.$SST_LINKS,
|
|
5
|
+
};
|
|
6
|
+
for (const [key, value] of Object.entries(env)) {
|
|
7
|
+
if (key.startsWith("SST_RESOURCE_") && value) {
|
|
8
|
+
raw[key.slice("SST_RESOURCE_".length)] = JSON.parse(value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function fromCloudflareEnv(input) {
|
|
12
|
+
for (const [key, value] of Object.entries(input)) {
|
|
13
|
+
raw[key] = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export const Resource = new Proxy(raw, {
|
|
17
|
+
get(_target, prop) {
|
|
18
|
+
if (prop in raw) {
|
|
19
|
+
return raw[prop];
|
|
11
20
|
}
|
|
12
21
|
throw new Error(`"${prop}" is not linked`);
|
|
13
22
|
},
|
package/dist/vector-client.d.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
+
import { Resource } from "./resource.js";
|
|
1
2
|
export type IngestEvent = {
|
|
2
|
-
/**
|
|
3
|
-
* The external ID of the event.
|
|
4
|
-
* If the external ID already exists, the embedding and metadata will be updated.
|
|
5
|
-
* @example
|
|
6
|
-
* ```js
|
|
7
|
-
* {
|
|
8
|
-
* externalId: "37043bc3-2166-437d-bbf8-a2238d7a5796"
|
|
9
|
-
* }
|
|
10
|
-
*/
|
|
11
|
-
externalId: string;
|
|
12
3
|
/**
|
|
13
4
|
* The text used to generate the embedding vector.
|
|
5
|
+
* At least one of `text` or `image` must be provided.
|
|
14
6
|
* @example
|
|
15
7
|
* ```js
|
|
16
8
|
* {
|
|
@@ -18,9 +10,10 @@ export type IngestEvent = {
|
|
|
18
10
|
* }
|
|
19
11
|
* ```
|
|
20
12
|
*/
|
|
21
|
-
text
|
|
13
|
+
text?: string;
|
|
22
14
|
/**
|
|
23
15
|
* The base64 representation of the image used to generate the embedding vector.
|
|
16
|
+
* At least one of `text` or `image` must be provided.
|
|
24
17
|
* @example
|
|
25
18
|
* ```js
|
|
26
19
|
* {
|
|
@@ -30,64 +23,107 @@ export type IngestEvent = {
|
|
|
30
23
|
*/
|
|
31
24
|
image?: string;
|
|
32
25
|
/**
|
|
33
|
-
*
|
|
34
|
-
* This metadata will be used to filter
|
|
26
|
+
* Metadata for the event in JSON format.
|
|
27
|
+
* This metadata will be used to filter when retrieving and removing embeddings.
|
|
35
28
|
* @example
|
|
36
29
|
* ```js
|
|
37
30
|
* {
|
|
38
31
|
* metadata: {
|
|
39
|
-
*
|
|
40
|
-
*
|
|
32
|
+
* type: "movie",
|
|
33
|
+
* id: "movie-123",
|
|
34
|
+
* name: "Spiderman",
|
|
41
35
|
* }
|
|
42
36
|
* }
|
|
43
37
|
* ```
|
|
44
38
|
*/
|
|
45
|
-
metadata: any
|
|
39
|
+
metadata: Record<string, any>;
|
|
46
40
|
};
|
|
47
41
|
export type RetrieveEvent = {
|
|
48
42
|
/**
|
|
49
|
-
* The prompt used to retrieve
|
|
43
|
+
* The text prompt used to retrieve embeddings.
|
|
44
|
+
* At least one of `text` or `image` must be provided.
|
|
50
45
|
* @example
|
|
51
46
|
* ```js
|
|
52
47
|
* {
|
|
53
|
-
*
|
|
48
|
+
* text: "This is an example text.",
|
|
54
49
|
* }
|
|
55
50
|
* ```
|
|
56
51
|
*/
|
|
57
|
-
|
|
52
|
+
text?: string;
|
|
58
53
|
/**
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
54
|
+
* The base64 representation of the image prompt used to retrive embeddings.
|
|
55
|
+
* At least one of `text` or `image` must be provided.
|
|
61
56
|
* @example
|
|
62
57
|
* ```js
|
|
63
58
|
* {
|
|
64
|
-
*
|
|
59
|
+
* image: await fs.readFile("./file.jpg").toString("base64"),
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
image?: string;
|
|
64
|
+
/**
|
|
65
|
+
* The metadata used to filter the retrieval of embeddings.
|
|
66
|
+
* Only embeddings with metadata that match the provided fields will be returned.
|
|
67
|
+
* @example
|
|
68
|
+
* ```js
|
|
69
|
+
* {
|
|
70
|
+
* include: {
|
|
65
71
|
* type: "movie",
|
|
66
|
-
*
|
|
72
|
+
* release: "2001",
|
|
67
73
|
* }
|
|
68
74
|
* }
|
|
69
75
|
* ```
|
|
70
|
-
* This will match
|
|
76
|
+
* This will match the embedding with metadata:
|
|
71
77
|
* {
|
|
72
78
|
* type: "movie",
|
|
73
79
|
* name: "Spiderman",
|
|
74
80
|
* release: "2001",
|
|
75
81
|
* }
|
|
76
|
-
*
|
|
82
|
+
*
|
|
83
|
+
* But not the embedding with metadata:
|
|
77
84
|
* {
|
|
78
85
|
* type: "book",
|
|
79
86
|
* name: "Spiderman",
|
|
80
|
-
* release: "
|
|
87
|
+
* release: "2001",
|
|
81
88
|
* }
|
|
82
89
|
*/
|
|
83
|
-
|
|
90
|
+
include: Record<string, any>;
|
|
84
91
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
92
|
+
* Exclude embeddings with metadata that match the provided fields.
|
|
93
|
+
* @example
|
|
94
|
+
* ```js
|
|
95
|
+
* {
|
|
96
|
+
* include: {
|
|
97
|
+
* type: "movie",
|
|
98
|
+
* release: "2001",
|
|
99
|
+
* },
|
|
100
|
+
* exclude: {
|
|
101
|
+
* name: "Spiderman",
|
|
102
|
+
* }
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
* This will match the embedding with metadata:
|
|
106
|
+
* {
|
|
107
|
+
* type: "movie",
|
|
108
|
+
* name: "A Beautiful Mind",
|
|
109
|
+
* release: "2001",
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* But not the embedding with metadata:
|
|
113
|
+
* {
|
|
114
|
+
* type: "book",
|
|
115
|
+
* name: "Spiderman",
|
|
116
|
+
* release: "2001",
|
|
117
|
+
* }
|
|
118
|
+
*/
|
|
119
|
+
exclude?: Record<string, any>;
|
|
120
|
+
/**
|
|
121
|
+
* The threshold of similarity between the prompt and the retrieved embeddings.
|
|
122
|
+
* Only embeddings with a similarity score higher than the threshold will be returned.
|
|
87
123
|
* Expected value is between 0 and 1.
|
|
88
|
-
* - 0 means the prompt and the retrieved
|
|
89
|
-
* - 1 means the prompt and the retrieved
|
|
90
|
-
* @default 0
|
|
124
|
+
* - 0 means the prompt and the retrieved embeddings are completely different.
|
|
125
|
+
* - 1 means the prompt and the retrieved embeddings are identical.
|
|
126
|
+
* @default `0`
|
|
91
127
|
* @example
|
|
92
128
|
* ```js
|
|
93
129
|
* {
|
|
@@ -98,7 +134,7 @@ export type RetrieveEvent = {
|
|
|
98
134
|
threshold?: number;
|
|
99
135
|
/**
|
|
100
136
|
* The number of results to return.
|
|
101
|
-
* @default 10
|
|
137
|
+
* @default `10`
|
|
102
138
|
* @example
|
|
103
139
|
* ```js
|
|
104
140
|
* {
|
|
@@ -110,17 +146,41 @@ export type RetrieveEvent = {
|
|
|
110
146
|
};
|
|
111
147
|
export type RemoveEvent = {
|
|
112
148
|
/**
|
|
113
|
-
* The
|
|
149
|
+
* The metadata used to filter the removal of embeddings.
|
|
150
|
+
* Only embeddings with metadata that match the provided fields will be removed.
|
|
114
151
|
* @example
|
|
152
|
+
* To remove embeddings for movie with id "movie-123":
|
|
115
153
|
* ```js
|
|
116
154
|
* {
|
|
117
|
-
*
|
|
155
|
+
* include: {
|
|
156
|
+
* id: "movie-123",
|
|
157
|
+
* }
|
|
118
158
|
* }
|
|
159
|
+
* ```
|
|
160
|
+
* To remove embeddings for all movies:
|
|
161
|
+
* {
|
|
162
|
+
* include: {
|
|
163
|
+
* type: "movie",
|
|
164
|
+
* }
|
|
165
|
+
* }
|
|
166
|
+
*/
|
|
167
|
+
include: Record<string, any>;
|
|
168
|
+
};
|
|
169
|
+
type RetriveResponse = {
|
|
170
|
+
/**
|
|
171
|
+
* Metadata for the event in JSON format that was provided when ingesting the embedding.
|
|
172
|
+
*/
|
|
173
|
+
metadata: Record<string, any>;
|
|
174
|
+
/**
|
|
175
|
+
* The similarity score between the prompt and the retrieved embedding.
|
|
119
176
|
*/
|
|
120
|
-
|
|
177
|
+
score: number;
|
|
121
178
|
};
|
|
122
|
-
export declare
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
179
|
+
export declare function VectorClient<T extends keyof {
|
|
180
|
+
[key in keyof Resource as "sst.aws.Vector" extends Resource[key]["type"] ? string extends key ? never : key : never]: Resource[key];
|
|
181
|
+
}>(name: T): {
|
|
182
|
+
ingest: (event: IngestEvent) => Promise<void>;
|
|
183
|
+
retrieve: (event: RetrieveEvent) => Promise<RetriveResponse>;
|
|
184
|
+
remove: (event: RemoveEvent) => Promise<void>;
|
|
126
185
|
};
|
|
186
|
+
export {};
|
package/dist/vector-client.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import { LambdaClient, InvokeCommand, } from "@aws-sdk/client-lambda";
|
|
2
2
|
import { Resource } from "./resource.js";
|
|
3
3
|
const lambda = new LambdaClient();
|
|
4
|
-
export
|
|
4
|
+
export function VectorClient(name) {
|
|
5
5
|
return {
|
|
6
6
|
ingest: async (event) => {
|
|
7
7
|
const ret = await lambda.send(new InvokeCommand({
|
|
8
|
-
FunctionName: Resource[name].
|
|
8
|
+
FunctionName: Resource[name].ingestor,
|
|
9
9
|
Payload: JSON.stringify(event),
|
|
10
10
|
}));
|
|
11
|
-
|
|
11
|
+
parsePayload(ret, "Failed to ingest into the vector db");
|
|
12
12
|
},
|
|
13
13
|
retrieve: async (event) => {
|
|
14
14
|
const ret = await lambda.send(new InvokeCommand({
|
|
15
|
-
FunctionName: Resource[name].
|
|
15
|
+
FunctionName: Resource[name].retriever,
|
|
16
16
|
Payload: JSON.stringify(event),
|
|
17
17
|
}));
|
|
18
18
|
return parsePayload(ret, "Failed to retrieve from the vector db");
|
|
19
19
|
},
|
|
20
|
-
|
|
20
|
+
remove: async (event) => {
|
|
21
21
|
const ret = await lambda.send(new InvokeCommand({
|
|
22
|
-
FunctionName: Resource[name].
|
|
22
|
+
FunctionName: Resource[name].remover,
|
|
23
23
|
Payload: JSON.stringify(event),
|
|
24
24
|
}));
|
|
25
|
-
|
|
25
|
+
parsePayload(ret, "Failed to remove from the vector db");
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
|
-
}
|
|
28
|
+
}
|
|
29
29
|
function parsePayload(output, message) {
|
|
30
30
|
const payload = JSON.parse(Buffer.from(output.Payload).toString());
|
|
31
31
|
// Set cause to the payload so that it can be logged in CloudWatch
|
package/package.json
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "sst",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"version": "3.0.3",
|
|
6
7
|
"main": "./dist/index.js",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": "./dist/index.js",
|
|
@@ -17,11 +18,14 @@
|
|
|
17
18
|
"dist"
|
|
18
19
|
],
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@aws-sdk/client-lambda": "3.478.0"
|
|
21
|
+
"@aws-sdk/client-lambda": "3.478.0",
|
|
22
|
+
"hono": "^4.0.1",
|
|
23
|
+
"jose": "^5.2.1",
|
|
24
|
+
"openid-client": "^5.6.4"
|
|
21
25
|
},
|
|
22
26
|
"scripts": {
|
|
23
27
|
"build": "tsc",
|
|
24
28
|
"dev": "tsc -w",
|
|
25
|
-
"release": "pnpm build && pnpm version
|
|
29
|
+
"release": "pnpm build && pnpm version patch && pnpm publish --no-git-checks --tag=ion --access=public"
|
|
26
30
|
}
|
|
27
31
|
}
|