@~lyre/auth 0.0.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/dist/admin.d.ts +146 -0
- package/dist/admin.js +193 -0
- package/dist/{chunk-QIAL6IDV.js → chunk-TQNQLX3C.js} +6 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +3 -1
- package/dist/nest.d.ts +85 -0
- package/dist/nest.js +134 -0
- package/dist/sveltekit.d.ts +4 -1
- package/dist/sveltekit.js +6 -5
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ and identity handling (HMAC-signed cookies, the accounts login → callback →
|
|
|
5
5
|
logout exchange, tenant resolution), plus an optional turnkey **SvelteKit**
|
|
6
6
|
adapter that wires it all up in a few lines.
|
|
7
7
|
|
|
8
|
-
Ships
|
|
8
|
+
Ships built ESM output from `dist/`, with type declarations.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -53,6 +53,43 @@ export const handle = createAuthHandle({
|
|
|
53
53
|
`@sveltejs/kit` is an **optional peer dependency** — only required if you import
|
|
54
54
|
the `/sveltekit` entry.
|
|
55
55
|
|
|
56
|
+
### `@~lyre/auth/nest` — Nest / Fastify adapter
|
|
57
|
+
|
|
58
|
+
For services on Nest + Fastify. Adds **no dependencies** — nothing here imports
|
|
59
|
+
`@nestjs/*` or `fastify`; you supply the guard class and DI wiring, the SDK
|
|
60
|
+
supplies the Accounts logic. Works from a plain Fastify app too: any request
|
|
61
|
+
whose `headers` is a plain object satisfies the input type.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createServiceKeyResolver, createUserSessionResolver, hasScopes }
|
|
65
|
+
from '@~lyre/auth/nest';
|
|
66
|
+
|
|
67
|
+
const keys = createServiceKeyResolver({ config: { baseUrl: ACCOUNTS_BASE_URL } });
|
|
68
|
+
const sessions = createUserSessionResolver({ config: { baseUrl: ACCOUNTS_BASE_URL } });
|
|
69
|
+
|
|
70
|
+
// In a Nest guard (req is the Fastify request):
|
|
71
|
+
const key = await keys.resolve(req);
|
|
72
|
+
if (key.status === 'invalid') throw new UnauthorizedException(); // present but bad
|
|
73
|
+
if (key.status === 'ok') {
|
|
74
|
+
if (!hasScopes(key.context, ['my-app:things:read'])) throw new ForbiddenException();
|
|
75
|
+
// key.context.accessibleApps tells you which apps this key may act on
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A user session bearer resolves to who + which app, from the token alone:
|
|
79
|
+
const user = await sessions.resolve(req);
|
|
80
|
+
// → { userId, sessionId, appId, tenantId, email, name } | null
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Semantics match `createServiceKeyHandle`: **absent key → `absent`** (fall through to
|
|
84
|
+
other auth), **present-but-invalid → `invalid`** (reject rather than silently
|
|
85
|
+
continue). Introspection is cached by `sha256` of the credential — negative
|
|
86
|
+
results too, so a bad key can't hammer Accounts. Neither resolver throws: a
|
|
87
|
+
transport failure reads as "not authenticated", so a flaky Accounts cannot 500
|
|
88
|
+
the consuming service.
|
|
89
|
+
|
|
90
|
+
`resolve()` needs `GET /api/auth/me` to return `session.appId` (Accounts ≥ the
|
|
91
|
+
release that added it); older deployments yield `appId: null`.
|
|
92
|
+
|
|
56
93
|
## Publishing
|
|
57
94
|
|
|
58
95
|
See [PUBLISHING.md](PUBLISHING.md). Published to npm via GitHub Actions Trusted
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
interface AccountsAdminOptions {
|
|
2
|
+
/** Accounts base URL, e.g. `https://accounts.example.com`. */
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
/** A user session id (bearer) for session-authed endpoints (invites, profile update, user list). */
|
|
5
|
+
sessionToken?: string;
|
|
6
|
+
/** A machine service key (`sk_…`) for `x-api-key`-authed reads (group members). */
|
|
7
|
+
serviceKey?: string;
|
|
8
|
+
/** Override the fetch implementation (defaults to the global). */
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
interface AccountsUser {
|
|
12
|
+
id: string;
|
|
13
|
+
email: string;
|
|
14
|
+
name?: string | null;
|
|
15
|
+
firstName?: string | null;
|
|
16
|
+
lastName?: string | null;
|
|
17
|
+
emailVerified?: boolean;
|
|
18
|
+
/** The user's avatar URL (from their latest OAuth sign-in with a picture, or their upload). */
|
|
19
|
+
avatarUrl?: string | null;
|
|
20
|
+
profileData?: Record<string, string>;
|
|
21
|
+
userGroups?: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
slug?: string;
|
|
24
|
+
displayName?: string;
|
|
25
|
+
role?: string;
|
|
26
|
+
}>;
|
|
27
|
+
apps?: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
}>;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
interface ChangePasswordResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
mode?: 'set' | 'changed';
|
|
37
|
+
revokedCount?: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
interface ListUsersQuery {
|
|
41
|
+
tenantId?: string;
|
|
42
|
+
appId?: string;
|
|
43
|
+
q?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
email?: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
offset?: number;
|
|
48
|
+
}
|
|
49
|
+
interface ListUsersResult {
|
|
50
|
+
users: AccountsUser[];
|
|
51
|
+
pagination?: {
|
|
52
|
+
total: number;
|
|
53
|
+
limit: number;
|
|
54
|
+
offset: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
interface AppMember {
|
|
58
|
+
id: string;
|
|
59
|
+
email: string;
|
|
60
|
+
name?: string | null;
|
|
61
|
+
via?: 'member' | 'role';
|
|
62
|
+
}
|
|
63
|
+
interface InviteResult {
|
|
64
|
+
/** `added` = the email already had an Accounts user and was granted app access immediately;
|
|
65
|
+
* `invited` = a new invite was created and an accept link emailed. */
|
|
66
|
+
status: 'added' | 'invited';
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
interface GroupMember {
|
|
70
|
+
id: string;
|
|
71
|
+
email: string;
|
|
72
|
+
name?: string | null;
|
|
73
|
+
firstName?: string | null;
|
|
74
|
+
lastName?: string | null;
|
|
75
|
+
role?: string | null;
|
|
76
|
+
}
|
|
77
|
+
/** Thrown on a non-2xx Accounts response so callers can surface the upstream message + status. */
|
|
78
|
+
declare class AccountsAdminError extends Error {
|
|
79
|
+
readonly status: number;
|
|
80
|
+
readonly body?: unknown | undefined;
|
|
81
|
+
constructor(status: number, message: string, body?: unknown | undefined);
|
|
82
|
+
}
|
|
83
|
+
declare class AccountsAdmin {
|
|
84
|
+
private readonly opts;
|
|
85
|
+
private readonly base;
|
|
86
|
+
private readonly f;
|
|
87
|
+
constructor(opts: AccountsAdminOptions);
|
|
88
|
+
/** List/search users in a tenant. Session-authed (caller must be privileged in the tenant). */
|
|
89
|
+
listUsers(query?: ListUsersQuery): Promise<ListUsersResult>;
|
|
90
|
+
/** One user's full detail (includes `profileData`, `userGroups`, `apps`). Session-authed. */
|
|
91
|
+
getUser(userId: string): Promise<AccountsUser>;
|
|
92
|
+
/** Update a user (self, or admin/owner/super-admin). `profileData` requires `appId`. Session-authed.
|
|
93
|
+
* Password is NOT set here — use {@link changePassword} (Accounts verifies the current password). */
|
|
94
|
+
updateUser(userId: string, input: {
|
|
95
|
+
name?: string;
|
|
96
|
+
email?: string;
|
|
97
|
+
appId?: string;
|
|
98
|
+
profileData?: Record<string, string>;
|
|
99
|
+
}): Promise<AccountsUser>;
|
|
100
|
+
/** Change the signed-in user's password. Session-authed; Accounts verifies `currentPassword` and,
|
|
101
|
+
* with `revokeOtherSessions`, logs the user out everywhere else. The target is the session's own
|
|
102
|
+
* user — no id is passed. */
|
|
103
|
+
changePassword(input: {
|
|
104
|
+
currentPassword: string;
|
|
105
|
+
newPassword: string;
|
|
106
|
+
confirmPassword?: string;
|
|
107
|
+
revokeOtherSessions?: boolean;
|
|
108
|
+
}): Promise<ChangePasswordResult>;
|
|
109
|
+
/** Invite an email to an app: adds an existing Accounts user immediately, else emails an accept
|
|
110
|
+
* link. Session-authed (caller must have access to the app). */
|
|
111
|
+
inviteToApp(appSlug: string, input: {
|
|
112
|
+
email: string;
|
|
113
|
+
}): Promise<InviteResult>;
|
|
114
|
+
/** Everyone with access to an app. Session-authed. */
|
|
115
|
+
listAppUsers(appSlug: string): Promise<AppMember[]>;
|
|
116
|
+
/** Revoke a user's access to an app. Session-authed. */
|
|
117
|
+
removeAppUser(appSlug: string, userId: string): Promise<void>;
|
|
118
|
+
/** Members of a user_group (the workspace's agents). Service-key authed (`stats:read`). */
|
|
119
|
+
listGroupMembers(groupId: string, opts?: {
|
|
120
|
+
appId?: string;
|
|
121
|
+
}): Promise<GroupMember[]>;
|
|
122
|
+
/** Read a user's app-scoped `profileData` (the store for per-app preferences). Session-authed. */
|
|
123
|
+
getAppProfile(userId: string): Promise<Record<string, string>>;
|
|
124
|
+
/** Write app-scoped `profileData` fields for a user (e.g. availability, soundEnabled). All values
|
|
125
|
+
* are stored as strings by Accounts; booleans are serialised. Session-authed. */
|
|
126
|
+
setAppProfile(userId: string, appId: string, profile: Record<string, string | number | boolean>): Promise<AccountsUser>;
|
|
127
|
+
/** Upload a user's avatar image. The bytes are stored by Accounts and a public URL is returned +
|
|
128
|
+
* persisted (source = 'upload', which survives later OAuth sign-ins). Session-authed, self-only.
|
|
129
|
+
* `image` is the file bytes (Blob/Buffer/Uint8Array); `mimeType` + `filename` describe it. */
|
|
130
|
+
uploadAvatar(userId: string, image: Blob | ArrayBuffer | Uint8Array, opts?: {
|
|
131
|
+
mimeType?: string;
|
|
132
|
+
filename?: string;
|
|
133
|
+
}): Promise<{
|
|
134
|
+
avatarUrl: string;
|
|
135
|
+
}>;
|
|
136
|
+
/** Remove a user's uploaded avatar (clears it, or reverts to their latest OAuth picture if any).
|
|
137
|
+
* Session-authed, self-only. */
|
|
138
|
+
removeAvatar(userId: string): Promise<{
|
|
139
|
+
avatarUrl: string | null;
|
|
140
|
+
}>;
|
|
141
|
+
private request;
|
|
142
|
+
}
|
|
143
|
+
/** Convenience factory mirroring the SDK's other `create*` helpers. */
|
|
144
|
+
declare function createAccountsAdmin(opts: AccountsAdminOptions): AccountsAdmin;
|
|
145
|
+
|
|
146
|
+
export { AccountsAdmin, AccountsAdminError, type AccountsAdminOptions, type AccountsUser, type AppMember, type ChangePasswordResult, type GroupMember, type InviteResult, type ListUsersQuery, type ListUsersResult, createAccountsAdmin };
|
package/dist/admin.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/admin.ts
|
|
2
|
+
var AccountsAdminError = class extends Error {
|
|
3
|
+
constructor(status, message, body) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.body = body;
|
|
7
|
+
this.name = "AccountsAdminError";
|
|
8
|
+
}
|
|
9
|
+
status;
|
|
10
|
+
body;
|
|
11
|
+
};
|
|
12
|
+
var AccountsAdmin = class {
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.opts = opts;
|
|
15
|
+
const base = (opts.baseUrl ?? "").trim();
|
|
16
|
+
if (!base) throw new Error("AccountsAdmin: baseUrl is required.");
|
|
17
|
+
this.base = base.replace(/\/+$/, "");
|
|
18
|
+
this.f = opts.fetchImpl ?? fetch;
|
|
19
|
+
}
|
|
20
|
+
opts;
|
|
21
|
+
base;
|
|
22
|
+
f;
|
|
23
|
+
// ── Users ──────────────────────────────────────────────────────────────────
|
|
24
|
+
/** List/search users in a tenant. Session-authed (caller must be privileged in the tenant). */
|
|
25
|
+
async listUsers(query = {}) {
|
|
26
|
+
const qs = new URLSearchParams();
|
|
27
|
+
for (const [k, v] of Object.entries(query)) {
|
|
28
|
+
if (v !== void 0 && v !== null) qs.set(k, String(v));
|
|
29
|
+
}
|
|
30
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
31
|
+
const data = await this.request(
|
|
32
|
+
"GET",
|
|
33
|
+
`/api/auth/users${suffix}`,
|
|
34
|
+
{ auth: "session" }
|
|
35
|
+
);
|
|
36
|
+
return { users: data.users ?? [], ...data.pagination ? { pagination: data.pagination } : {} };
|
|
37
|
+
}
|
|
38
|
+
/** One user's full detail (includes `profileData`, `userGroups`, `apps`). Session-authed. */
|
|
39
|
+
async getUser(userId) {
|
|
40
|
+
return this.request("GET", `/api/auth/users/${encodeURIComponent(userId)}`, {
|
|
41
|
+
auth: "session"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Update a user (self, or admin/owner/super-admin). `profileData` requires `appId`. Session-authed.
|
|
45
|
+
* Password is NOT set here — use {@link changePassword} (Accounts verifies the current password). */
|
|
46
|
+
async updateUser(userId, input) {
|
|
47
|
+
return this.request("PATCH", `/api/auth/users/${encodeURIComponent(userId)}`, {
|
|
48
|
+
auth: "session",
|
|
49
|
+
body: input
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Change the signed-in user's password. Session-authed; Accounts verifies `currentPassword` and,
|
|
53
|
+
* with `revokeOtherSessions`, logs the user out everywhere else. The target is the session's own
|
|
54
|
+
* user — no id is passed. */
|
|
55
|
+
async changePassword(input) {
|
|
56
|
+
return this.request("POST", "/api/auth/change-password", {
|
|
57
|
+
auth: "session",
|
|
58
|
+
body: {
|
|
59
|
+
currentPassword: input.currentPassword,
|
|
60
|
+
newPassword: input.newPassword,
|
|
61
|
+
confirmPassword: input.confirmPassword ?? input.newPassword,
|
|
62
|
+
...input.revokeOtherSessions !== void 0 ? { revokeOtherSessions: input.revokeOtherSessions } : {}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// ── App membership (invite / list / remove) ─────────────────────────────────
|
|
67
|
+
/** Invite an email to an app: adds an existing Accounts user immediately, else emails an accept
|
|
68
|
+
* link. Session-authed (caller must have access to the app). */
|
|
69
|
+
async inviteToApp(appSlug, input) {
|
|
70
|
+
return this.request("POST", `/api/auth/apps/${encodeURIComponent(appSlug)}/invites`, {
|
|
71
|
+
auth: "session",
|
|
72
|
+
body: input
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Everyone with access to an app. Session-authed. */
|
|
76
|
+
async listAppUsers(appSlug) {
|
|
77
|
+
const data = await this.request(
|
|
78
|
+
"GET",
|
|
79
|
+
`/api/auth/apps/${encodeURIComponent(appSlug)}/users`,
|
|
80
|
+
{ auth: "session" }
|
|
81
|
+
);
|
|
82
|
+
return data.users ?? [];
|
|
83
|
+
}
|
|
84
|
+
/** Revoke a user's access to an app. Session-authed. */
|
|
85
|
+
async removeAppUser(appSlug, userId) {
|
|
86
|
+
await this.request(
|
|
87
|
+
"DELETE",
|
|
88
|
+
`/api/auth/apps/${encodeURIComponent(appSlug)}/users?userId=${encodeURIComponent(userId)}`,
|
|
89
|
+
{ auth: "session" }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// ── Groups (agent picker) ───────────────────────────────────────────────────
|
|
93
|
+
/** Members of a user_group (the workspace's agents). Service-key authed (`stats:read`). */
|
|
94
|
+
async listGroupMembers(groupId, opts = {}) {
|
|
95
|
+
const suffix = opts.appId ? `?appId=${encodeURIComponent(opts.appId)}` : "";
|
|
96
|
+
const data = await this.request(
|
|
97
|
+
"GET",
|
|
98
|
+
`/api/auth/user-groups/${encodeURIComponent(groupId)}/members${suffix}`,
|
|
99
|
+
{ auth: "key" }
|
|
100
|
+
);
|
|
101
|
+
return data.members ?? [];
|
|
102
|
+
}
|
|
103
|
+
// ── App-scoped profile (availability / sound / any per-app preference) ───────
|
|
104
|
+
/** Read a user's app-scoped `profileData` (the store for per-app preferences). Session-authed. */
|
|
105
|
+
async getAppProfile(userId) {
|
|
106
|
+
const user = await this.getUser(userId);
|
|
107
|
+
return user.profileData ?? {};
|
|
108
|
+
}
|
|
109
|
+
/** Write app-scoped `profileData` fields for a user (e.g. availability, soundEnabled). All values
|
|
110
|
+
* are stored as strings by Accounts; booleans are serialised. Session-authed. */
|
|
111
|
+
async setAppProfile(userId, appId, profile) {
|
|
112
|
+
const profileData = {};
|
|
113
|
+
for (const [k, v] of Object.entries(profile)) profileData[k] = String(v);
|
|
114
|
+
return this.updateUser(userId, { appId, profileData });
|
|
115
|
+
}
|
|
116
|
+
// ── Avatar (uploaded image; social avatars are captured server-side on sign-in) ─
|
|
117
|
+
/** Upload a user's avatar image. The bytes are stored by Accounts and a public URL is returned +
|
|
118
|
+
* persisted (source = 'upload', which survives later OAuth sign-ins). Session-authed, self-only.
|
|
119
|
+
* `image` is the file bytes (Blob/Buffer/Uint8Array); `mimeType` + `filename` describe it. */
|
|
120
|
+
async uploadAvatar(userId, image, opts = {}) {
|
|
121
|
+
if (!this.opts.sessionToken) {
|
|
122
|
+
throw new Error("AccountsAdmin: a session token is required to upload an avatar.");
|
|
123
|
+
}
|
|
124
|
+
const mimeType = opts.mimeType ?? "application/octet-stream";
|
|
125
|
+
const blob = image instanceof Blob ? image : new Blob([image], { type: mimeType });
|
|
126
|
+
const form = new FormData();
|
|
127
|
+
form.append("avatar", blob, opts.filename ?? "avatar");
|
|
128
|
+
const res = await this.f(
|
|
129
|
+
new URL(`/api/auth/users/${encodeURIComponent(userId)}/avatar`, this.base),
|
|
130
|
+
{
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { authorization: `Bearer ${this.opts.sessionToken}`, accept: "application/json" },
|
|
133
|
+
body: form
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
const text = await res.text();
|
|
137
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
throw new AccountsAdminError(res.status, parsed?.error ?? "Avatar upload failed.", parsed);
|
|
140
|
+
}
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
/** Remove a user's uploaded avatar (clears it, or reverts to their latest OAuth picture if any).
|
|
144
|
+
* Session-authed, self-only. */
|
|
145
|
+
async removeAvatar(userId) {
|
|
146
|
+
return this.request(
|
|
147
|
+
"DELETE",
|
|
148
|
+
`/api/auth/users/${encodeURIComponent(userId)}/avatar`,
|
|
149
|
+
{ auth: "session" }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
// ── internals ───────────────────────────────────────────────────────────────
|
|
153
|
+
async request(method, path, opts) {
|
|
154
|
+
const headers = { accept: "application/json" };
|
|
155
|
+
if (opts.auth === "session") {
|
|
156
|
+
if (!this.opts.sessionToken) {
|
|
157
|
+
throw new Error(`AccountsAdmin: a session token is required for ${method} ${path}.`);
|
|
158
|
+
}
|
|
159
|
+
headers.authorization = `Bearer ${this.opts.sessionToken}`;
|
|
160
|
+
} else {
|
|
161
|
+
if (!this.opts.serviceKey) {
|
|
162
|
+
throw new Error(`AccountsAdmin: a service key is required for ${method} ${path}.`);
|
|
163
|
+
}
|
|
164
|
+
headers["x-api-key"] = this.opts.serviceKey;
|
|
165
|
+
}
|
|
166
|
+
let bodyStr;
|
|
167
|
+
if (opts.body !== void 0) {
|
|
168
|
+
bodyStr = JSON.stringify(opts.body);
|
|
169
|
+
headers["content-type"] = "application/json";
|
|
170
|
+
}
|
|
171
|
+
const res = await this.f(new URL(path, this.base), { method, headers, body: bodyStr });
|
|
172
|
+
const text = await res.text();
|
|
173
|
+
let parsed;
|
|
174
|
+
try {
|
|
175
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
176
|
+
} catch {
|
|
177
|
+
parsed = text;
|
|
178
|
+
}
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : void 0) ?? `Accounts request failed with status ${res.status}.`;
|
|
181
|
+
throw new AccountsAdminError(res.status, message, parsed);
|
|
182
|
+
}
|
|
183
|
+
return parsed;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function createAccountsAdmin(opts) {
|
|
187
|
+
return new AccountsAdmin(opts);
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
AccountsAdmin,
|
|
191
|
+
AccountsAdminError,
|
|
192
|
+
createAccountsAdmin
|
|
193
|
+
};
|
|
@@ -308,6 +308,11 @@ function verifyWebhookSignature(rawBody, signature, secret) {
|
|
|
308
308
|
if (a.length !== b.length) return false;
|
|
309
309
|
return timingSafeEqual(a, b);
|
|
310
310
|
}
|
|
311
|
+
function normalizeAccountsWebhook(payload) {
|
|
312
|
+
const body = payload && typeof payload === "object" ? payload : {};
|
|
313
|
+
const type = String(body.event ?? body.type ?? "unknown");
|
|
314
|
+
return { ...body, type, data: body.data };
|
|
315
|
+
}
|
|
311
316
|
function scopeSatisfied(granted, required) {
|
|
312
317
|
if (!granted || granted.length === 0) return false;
|
|
313
318
|
return granted.includes(required);
|
|
@@ -328,5 +333,6 @@ export {
|
|
|
328
333
|
introspectServiceKey,
|
|
329
334
|
logoutFromAccounts,
|
|
330
335
|
verifyWebhookSignature,
|
|
336
|
+
normalizeAccountsWebhook,
|
|
331
337
|
scopeSatisfied
|
|
332
338
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -174,8 +174,18 @@ declare function logoutFromAccounts(input: {
|
|
|
174
174
|
declare function verifyWebhookSignature(rawBody: string, signature: string | null | undefined, secret: string): boolean;
|
|
175
175
|
type AccountsWebhookEvent = {
|
|
176
176
|
type: string;
|
|
177
|
+
data?: unknown;
|
|
177
178
|
[key: string]: unknown;
|
|
178
179
|
};
|
|
180
|
+
/**
|
|
181
|
+
* Normalize an Accounts webhook body into a stable `AccountsWebhookEvent`.
|
|
182
|
+
*
|
|
183
|
+
* The Accounts webhook worker sends `{ event, timestamp, data }` (the event NAME under `event`,
|
|
184
|
+
* the payload under `data`). Older/other senders may put the name under `type`. This collapses both
|
|
185
|
+
* so a consumer always reads `event.type` and `event.data`, and keeps the raw fields alongside.
|
|
186
|
+
* Framework-agnostic so both the SvelteKit handle and the Nest helper share one shape.
|
|
187
|
+
*/
|
|
188
|
+
declare function normalizeAccountsWebhook(payload: unknown): AccountsWebhookEvent;
|
|
179
189
|
declare function scopeSatisfied(granted: string[] | undefined, required: string): boolean;
|
|
180
190
|
|
|
181
|
-
export { type AccountsClientConfig, type AccountsIdentity, type AccountsTokenResponse, type AccountsWebhookEvent, type AuthenticatedPrincipal, type AuthorizationState, type CookieOptions, type LocalUser, type PermissionSet, type PlatformSession, type ServiceKeyApp, type ServiceKeyContext, type SyncAccountsUser, type SyncAccountsUserResult, type TenantAccessContext, type TenantContext, type TenantMembership, beginAccountsLoginRedirect, clearPlatformSessionCookie, createAccountsClientConfig, createPlatformAuth, exchangeAuthorizationCode, handleAccountsCallback, identityPassthroughSync, introspectServiceKey, logoutFromAccounts, readPlatformSessionCookie, resolveActiveTenant, scopeSatisfied, serviceKeyFromRequest, syncAccountsUser, verifyWebhookSignature };
|
|
191
|
+
export { type AccountsClientConfig, type AccountsIdentity, type AccountsTokenResponse, type AccountsWebhookEvent, type AuthenticatedPrincipal, type AuthorizationState, type CookieOptions, type LocalUser, type PermissionSet, type PlatformSession, type ServiceKeyApp, type ServiceKeyContext, type SyncAccountsUser, type SyncAccountsUserResult, type TenantAccessContext, type TenantContext, type TenantMembership, beginAccountsLoginRedirect, clearPlatformSessionCookie, createAccountsClientConfig, createPlatformAuth, exchangeAuthorizationCode, handleAccountsCallback, identityPassthroughSync, introspectServiceKey, logoutFromAccounts, normalizeAccountsWebhook, readPlatformSessionCookie, resolveActiveTenant, scopeSatisfied, serviceKeyFromRequest, syncAccountsUser, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
identityPassthroughSync,
|
|
9
9
|
introspectServiceKey,
|
|
10
10
|
logoutFromAccounts,
|
|
11
|
+
normalizeAccountsWebhook,
|
|
11
12
|
readPlatformSessionCookie,
|
|
12
13
|
resolveActiveTenant,
|
|
13
14
|
scopeSatisfied,
|
|
14
15
|
serviceKeyFromRequest,
|
|
15
16
|
syncAccountsUser,
|
|
16
17
|
verifyWebhookSignature
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-TQNQLX3C.js";
|
|
18
19
|
export {
|
|
19
20
|
beginAccountsLoginRedirect,
|
|
20
21
|
clearPlatformSessionCookie,
|
|
@@ -25,6 +26,7 @@ export {
|
|
|
25
26
|
identityPassthroughSync,
|
|
26
27
|
introspectServiceKey,
|
|
27
28
|
logoutFromAccounts,
|
|
29
|
+
normalizeAccountsWebhook,
|
|
28
30
|
readPlatformSessionCookie,
|
|
29
31
|
resolveActiveTenant,
|
|
30
32
|
scopeSatisfied,
|
package/dist/nest.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AccountsWebhookEvent, ServiceKeyContext, AccountsClientConfig } from './index.js';
|
|
2
|
+
export { normalizeAccountsWebhook } from './index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The minimum a request must expose. Fastify's `request` satisfies this as-is
|
|
6
|
+
* (`headers` is a plain object), as does Node's IncomingMessage.
|
|
7
|
+
*/
|
|
8
|
+
type HeaderCarrier = {
|
|
9
|
+
headers: Record<string, string | string[] | undefined>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Adapt a header-bearing request to the web `Request` that the core helpers expect.
|
|
13
|
+
* Only headers are needed — `serviceKeyFromRequest` reads `x-api-key` / `authorization`.
|
|
14
|
+
*/
|
|
15
|
+
declare function toWebRequest(carrier: HeaderCarrier): Request;
|
|
16
|
+
/** Extract an `sk_` service key from a Fastify/Node request, or null when absent. */
|
|
17
|
+
declare function serviceKeyFromHeaders(carrier: HeaderCarrier): string | null;
|
|
18
|
+
type ServiceKeyResolverOptions = {
|
|
19
|
+
config: Pick<AccountsClientConfig, 'baseUrl'>;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
/** Introspection cache TTL in ms. Default 60_000; 0 disables caching. */
|
|
22
|
+
cacheTtlMs?: number;
|
|
23
|
+
};
|
|
24
|
+
type ServiceKeyResolution = {
|
|
25
|
+
status: 'absent';
|
|
26
|
+
} | {
|
|
27
|
+
status: 'invalid';
|
|
28
|
+
} | {
|
|
29
|
+
status: 'ok';
|
|
30
|
+
context: ServiceKeyContext;
|
|
31
|
+
};
|
|
32
|
+
declare function createServiceKeyResolver(opts: ServiceKeyResolverOptions): {
|
|
33
|
+
resolve: (carrier: HeaderCarrier) => Promise<ServiceKeyResolution>;
|
|
34
|
+
invalidate: (key: string) => void;
|
|
35
|
+
};
|
|
36
|
+
type UserSessionContext = {
|
|
37
|
+
userId: string;
|
|
38
|
+
email?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
/** The app the session was issued for. Null for sessions created outside an app context. */
|
|
42
|
+
appId: string | null;
|
|
43
|
+
tenantId: string | null;
|
|
44
|
+
expiresAt?: string;
|
|
45
|
+
};
|
|
46
|
+
type UserSessionResolverOptions = {
|
|
47
|
+
config: Pick<AccountsClientConfig, 'baseUrl'>;
|
|
48
|
+
fetchImpl?: typeof fetch;
|
|
49
|
+
/** Cache TTL in ms. Default 60_000; 0 disables caching. */
|
|
50
|
+
cacheTtlMs?: number;
|
|
51
|
+
};
|
|
52
|
+
/** A session bearer is any Authorization: Bearer token that is NOT an `sk_` service key. */
|
|
53
|
+
declare function sessionTokenFromHeaders(carrier: HeaderCarrier): string | null;
|
|
54
|
+
declare function createUserSessionResolver(opts: UserSessionResolverOptions): {
|
|
55
|
+
resolve: (carrier: HeaderCarrier) => Promise<UserSessionContext | null>;
|
|
56
|
+
invalidate: (token: string) => void;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Whether a resolved key grants every required scope. Uses the core's `scopeSatisfied`
|
|
60
|
+
* (flat exact match, no wildcards) so behaviour matches Accounts' own enforcement.
|
|
61
|
+
*/
|
|
62
|
+
declare function hasScopes(ctx: ServiceKeyContext, required: readonly string[]): boolean;
|
|
63
|
+
/** Whether a resolved key may act on an app, matched by Accounts app id OR slug. */
|
|
64
|
+
declare function canAccessApp(ctx: ServiceKeyContext, appIdOrSlug: string): boolean;
|
|
65
|
+
type AccountsWebhookResult = {
|
|
66
|
+
ok: true;
|
|
67
|
+
event: AccountsWebhookEvent;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: 'bad_signature' | 'bad_json';
|
|
71
|
+
};
|
|
72
|
+
/** Header the Accounts webhook worker sends (HMAC-SHA256 hex of the raw body). */
|
|
73
|
+
declare const ACCOUNTS_WEBHOOK_SIGNATURE_HEADER = "x-webhook-signature";
|
|
74
|
+
/**
|
|
75
|
+
* Verify an Accounts webhook and parse it into a normalized `{ type, data, ... }` event.
|
|
76
|
+
*
|
|
77
|
+
* Pass the RAW request body (a Nest/Fastify controller must read the raw body, not a re-serialized
|
|
78
|
+
* object — the signature is over the exact bytes), the signature header value
|
|
79
|
+
* (`request.headers['x-webhook-signature']`), and the shared secret. Returns `{ok:false}` on a bad
|
|
80
|
+
* signature or unparseable JSON so the controller can 401/400; `{ok:true, event}` otherwise. The
|
|
81
|
+
* controller then reacts to `event.type` / `event.data` and returns 200 (so Accounts doesn't retry).
|
|
82
|
+
*/
|
|
83
|
+
declare function verifyAndParseAccountsWebhook(rawBody: string, signatureHeaderValue: string | string[] | undefined, secret: string): AccountsWebhookResult;
|
|
84
|
+
|
|
85
|
+
export { ACCOUNTS_WEBHOOK_SIGNATURE_HEADER, AccountsClientConfig, AccountsWebhookEvent, type AccountsWebhookResult, type HeaderCarrier, ServiceKeyContext, type ServiceKeyResolution, type ServiceKeyResolverOptions, type UserSessionContext, type UserSessionResolverOptions, canAccessApp, createServiceKeyResolver, createUserSessionResolver, hasScopes, serviceKeyFromHeaders, sessionTokenFromHeaders, toWebRequest, verifyAndParseAccountsWebhook };
|
package/dist/nest.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
introspectServiceKey,
|
|
3
|
+
normalizeAccountsWebhook,
|
|
4
|
+
scopeSatisfied,
|
|
5
|
+
serviceKeyFromRequest,
|
|
6
|
+
verifyWebhookSignature
|
|
7
|
+
} from "./chunk-TQNQLX3C.js";
|
|
8
|
+
|
|
9
|
+
// src/nest.ts
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
function headerValue(carrier, name) {
|
|
12
|
+
const v = carrier.headers[name] ?? carrier.headers[name.toLowerCase()];
|
|
13
|
+
return Array.isArray(v) ? v[0] : v;
|
|
14
|
+
}
|
|
15
|
+
function toWebRequest(carrier) {
|
|
16
|
+
const headers = new Headers();
|
|
17
|
+
for (const [k, v] of Object.entries(carrier.headers)) {
|
|
18
|
+
if (v === void 0) continue;
|
|
19
|
+
if (Array.isArray(v)) {
|
|
20
|
+
for (const item of v) headers.append(k, item);
|
|
21
|
+
} else {
|
|
22
|
+
headers.set(k, v);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return new Request("http://localhost/", { headers });
|
|
26
|
+
}
|
|
27
|
+
function serviceKeyFromHeaders(carrier) {
|
|
28
|
+
return serviceKeyFromRequest(toWebRequest(carrier));
|
|
29
|
+
}
|
|
30
|
+
function createServiceKeyResolver(opts) {
|
|
31
|
+
const ttl = opts.cacheTtlMs ?? 6e4;
|
|
32
|
+
const cache = /* @__PURE__ */ new Map();
|
|
33
|
+
async function resolve(carrier) {
|
|
34
|
+
const key = serviceKeyFromHeaders(carrier);
|
|
35
|
+
if (!key) return { status: "absent" };
|
|
36
|
+
const hash = createHash("sha256").update(key).digest("hex");
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
let ctx;
|
|
39
|
+
const cached = ttl > 0 ? cache.get(hash) : void 0;
|
|
40
|
+
if (cached && cached.expires > now) {
|
|
41
|
+
ctx = cached.value;
|
|
42
|
+
} else {
|
|
43
|
+
ctx = await introspectServiceKey({ config: opts.config, key, fetchImpl: opts.fetchImpl });
|
|
44
|
+
if (ttl > 0) cache.set(hash, { value: ctx, expires: now + ttl });
|
|
45
|
+
}
|
|
46
|
+
return ctx ? { status: "ok", context: ctx } : { status: "invalid" };
|
|
47
|
+
}
|
|
48
|
+
function invalidate(key) {
|
|
49
|
+
cache.delete(createHash("sha256").update(key).digest("hex"));
|
|
50
|
+
}
|
|
51
|
+
return { resolve, invalidate };
|
|
52
|
+
}
|
|
53
|
+
function sessionTokenFromHeaders(carrier) {
|
|
54
|
+
const auth = headerValue(carrier, "authorization");
|
|
55
|
+
if (!auth || !auth.startsWith("Bearer ")) return null;
|
|
56
|
+
const token = auth.slice(7).trim();
|
|
57
|
+
if (!token || token.startsWith("sk_")) return null;
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
function createUserSessionResolver(opts) {
|
|
61
|
+
const ttl = opts.cacheTtlMs ?? 6e4;
|
|
62
|
+
const cache = /* @__PURE__ */ new Map();
|
|
63
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
64
|
+
async function introspect(token) {
|
|
65
|
+
const base = opts.config.baseUrl?.replace(/\/$/, "");
|
|
66
|
+
if (!base || !token) return null;
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetchImpl(new URL("/api/auth/me", base), {
|
|
69
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" }
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) return null;
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
const userId = data.user?.id;
|
|
74
|
+
const sessionId = data.session?.id;
|
|
75
|
+
if (!userId || !sessionId) return null;
|
|
76
|
+
return {
|
|
77
|
+
userId,
|
|
78
|
+
email: data.user?.emailAddress ?? data.user?.email,
|
|
79
|
+
name: data.user?.name,
|
|
80
|
+
sessionId,
|
|
81
|
+
appId: data.session?.appId ?? null,
|
|
82
|
+
tenantId: data.session?.tenantId ?? null,
|
|
83
|
+
expiresAt: data.session?.expiresAt
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function resolve(carrier) {
|
|
90
|
+
const token = sessionTokenFromHeaders(carrier);
|
|
91
|
+
if (!token) return null;
|
|
92
|
+
const hash = createHash("sha256").update(token).digest("hex");
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const cached = ttl > 0 ? cache.get(hash) : void 0;
|
|
95
|
+
if (cached && cached.expires > now) return cached.value;
|
|
96
|
+
const ctx = await introspect(token);
|
|
97
|
+
if (ttl > 0) cache.set(hash, { value: ctx, expires: now + ttl });
|
|
98
|
+
return ctx;
|
|
99
|
+
}
|
|
100
|
+
function invalidate(token) {
|
|
101
|
+
cache.delete(createHash("sha256").update(token).digest("hex"));
|
|
102
|
+
}
|
|
103
|
+
return { resolve, invalidate };
|
|
104
|
+
}
|
|
105
|
+
function hasScopes(ctx, required) {
|
|
106
|
+
return required.every((scope) => scopeSatisfied(ctx.scopes, scope));
|
|
107
|
+
}
|
|
108
|
+
function canAccessApp(ctx, appIdOrSlug) {
|
|
109
|
+
return ctx.accessibleApps.some((a) => a.id === appIdOrSlug || a.slug === appIdOrSlug);
|
|
110
|
+
}
|
|
111
|
+
var ACCOUNTS_WEBHOOK_SIGNATURE_HEADER = "x-webhook-signature";
|
|
112
|
+
function verifyAndParseAccountsWebhook(rawBody, signatureHeaderValue, secret) {
|
|
113
|
+
const signature = Array.isArray(signatureHeaderValue) ? signatureHeaderValue[0] : signatureHeaderValue;
|
|
114
|
+
if (!verifyWebhookSignature(rawBody, signature ?? null, secret)) {
|
|
115
|
+
return { ok: false, reason: "bad_signature" };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return { ok: true, event: normalizeAccountsWebhook(JSON.parse(rawBody)) };
|
|
119
|
+
} catch {
|
|
120
|
+
return { ok: false, reason: "bad_json" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
ACCOUNTS_WEBHOOK_SIGNATURE_HEADER,
|
|
125
|
+
canAccessApp,
|
|
126
|
+
createServiceKeyResolver,
|
|
127
|
+
createUserSessionResolver,
|
|
128
|
+
hasScopes,
|
|
129
|
+
normalizeAccountsWebhook,
|
|
130
|
+
serviceKeyFromHeaders,
|
|
131
|
+
sessionTokenFromHeaders,
|
|
132
|
+
toWebRequest,
|
|
133
|
+
verifyAndParseAccountsWebhook
|
|
134
|
+
};
|
package/dist/sveltekit.d.ts
CHANGED
|
@@ -55,7 +55,10 @@ type AccountsWebhookOptions = {
|
|
|
55
55
|
secret: string;
|
|
56
56
|
/** Route to receive webhooks on. Default '/api/auth/accounts/webhook'. */
|
|
57
57
|
path?: string;
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Header carrying the signature. Default 'x-webhook-signature' — the header the Accounts
|
|
60
|
+
* webhook worker actually sends (HMAC-SHA256 hex of the raw body).
|
|
61
|
+
*/
|
|
59
62
|
signatureHeader?: string;
|
|
60
63
|
/** React to a verified event. Throwing yields a 500 (Accounts may retry). */
|
|
61
64
|
onEvent: (event: AccountsWebhookEvent, raw: string, request: RequestEvent) => void | Promise<void>;
|
package/dist/sveltekit.js
CHANGED
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
identityPassthroughSync,
|
|
6
6
|
introspectServiceKey,
|
|
7
7
|
logoutFromAccounts,
|
|
8
|
+
normalizeAccountsWebhook,
|
|
8
9
|
readPlatformSessionCookie,
|
|
9
10
|
serviceKeyFromRequest,
|
|
10
11
|
verifyWebhookSignature
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-TQNQLX3C.js";
|
|
12
13
|
|
|
13
14
|
// src/sveltekit.ts
|
|
14
15
|
import { createHash } from "crypto";
|
|
@@ -113,21 +114,21 @@ function jsonResponse(status, body) {
|
|
|
113
114
|
}
|
|
114
115
|
function createAccountsWebhookHandle(opts) {
|
|
115
116
|
const path = opts.path ?? "/api/auth/accounts/webhook";
|
|
116
|
-
const sigHeader = opts.signatureHeader ?? "x-
|
|
117
|
+
const sigHeader = opts.signatureHeader ?? "x-webhook-signature";
|
|
117
118
|
return async ({ event, resolve }) => {
|
|
118
119
|
if (event.url.pathname !== path || event.request.method !== "POST") return resolve(event);
|
|
119
120
|
const raw = await event.request.text();
|
|
120
121
|
if (!verifyWebhookSignature(raw, event.request.headers.get(sigHeader), opts.secret)) {
|
|
121
122
|
return jsonResponse(401, { ok: false, error: "Invalid webhook signature." });
|
|
122
123
|
}
|
|
123
|
-
let
|
|
124
|
+
let parsed;
|
|
124
125
|
try {
|
|
125
|
-
|
|
126
|
+
parsed = normalizeAccountsWebhook(JSON.parse(raw));
|
|
126
127
|
} catch {
|
|
127
128
|
return jsonResponse(400, { ok: false, error: "Invalid JSON body." });
|
|
128
129
|
}
|
|
129
130
|
try {
|
|
130
|
-
await opts.onEvent(
|
|
131
|
+
await opts.onEvent(parsed, raw, event);
|
|
131
132
|
} catch {
|
|
132
133
|
return jsonResponse(500, { ok: false, error: "Webhook handler failed." });
|
|
133
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@~lyre/auth",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared Axis Accounts auth SDK — framework-agnostic session/identity core (HMAC-signed cookies, accounts login/callback/logout) plus an optional turnkey SvelteKit adapter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -33,7 +33,16 @@
|
|
|
33
33
|
"./sveltekit": {
|
|
34
34
|
"types": "./dist/sveltekit.d.ts",
|
|
35
35
|
"import": "./dist/sveltekit.js"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"./nest": {
|
|
38
|
+
"types": "./dist/nest.d.ts",
|
|
39
|
+
"import": "./dist/nest.js"
|
|
40
|
+
},
|
|
41
|
+
"./admin": {
|
|
42
|
+
"types": "./dist/admin.d.ts",
|
|
43
|
+
"import": "./dist/admin.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
37
46
|
},
|
|
38
47
|
"files": [
|
|
39
48
|
"dist",
|
|
@@ -41,7 +50,7 @@
|
|
|
41
50
|
"LICENSE"
|
|
42
51
|
],
|
|
43
52
|
"scripts": {
|
|
44
|
-
"build": "tsup src/index.ts src/sveltekit.ts --format esm --dts --clean",
|
|
53
|
+
"build": "tsup src/index.ts src/sveltekit.ts src/nest.ts src/admin.ts --format esm --dts --clean",
|
|
45
54
|
"check": "tsc --noEmit"
|
|
46
55
|
},
|
|
47
56
|
"peerDependencies": {
|