@pablozaiden/webapp 0.1.0 → 0.2.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.
@@ -1,7 +1,26 @@
1
- import type { ApiKeySummary, LogLevelName, ThemePreference } from "../../contracts";
1
+ import type { ApiKeySummary, AuditEventSummary, LogLevelName, ThemePreference, WebAppUserRole, WebAppUserSummary } from "../../contracts";
2
+
3
+ export interface UserRecord extends WebAppUserSummary {
4
+ authVersion: number;
5
+ disabledAt?: string;
6
+ }
7
+
8
+ export interface UserSetupLinkRecord {
9
+ id: string;
10
+ userId: string;
11
+ tokenHash: string;
12
+ kind: "invite" | "reset" | "owner-reset";
13
+ createdByUserId?: string;
14
+ createdAt: string;
15
+ expiresAt: string;
16
+ consumedAt?: string;
17
+ }
18
+
19
+ export interface AuditEventRecord extends AuditEventSummary {}
2
20
 
3
21
  export interface StoredPasskey {
4
22
  id: string;
23
+ userId: string;
5
24
  name: string;
6
25
  credentialId: string;
7
26
  publicKey: Uint8Array<ArrayBuffer>;
@@ -15,6 +34,7 @@ export interface StoredPasskey {
15
34
  }
16
35
 
17
36
  export interface ApiKeyRecord extends ApiKeySummary {
37
+ userId: string;
18
38
  tokenHash: string;
19
39
  }
20
40
 
@@ -24,6 +44,7 @@ export interface DeviceAuthRequestRecord {
24
44
  clientId: string;
25
45
  scope: string;
26
46
  status: "pending" | "approved" | "denied" | "consumed";
47
+ approvedByUserId?: string;
27
48
  createdAt: string;
28
49
  updatedAt: string;
29
50
  expiresAt: string;
@@ -31,6 +52,7 @@ export interface DeviceAuthRequestRecord {
31
52
 
32
53
  export interface RefreshSessionRecord {
33
54
  id: string;
55
+ userId: string;
34
56
  familyId: string;
35
57
  clientId: string;
36
58
  scope: string;
@@ -52,31 +74,52 @@ export interface SigningKeyRecord {
52
74
 
53
75
  export interface WebAppStore {
54
76
  initialize(): void;
55
- getPreference(key: string): string | undefined;
56
- setPreference(key: string, value: string): void;
57
- deletePreference(key: string): void;
77
+ getPreference(key: string, userId?: string): string | undefined;
78
+ setPreference(key: string, value: string, userId?: string): void;
79
+ deletePreference(key: string, userId?: string): void;
58
80
 
59
- getThemePreference(): ThemePreference | undefined;
60
- setThemePreference(value: ThemePreference): void;
81
+ getThemePreference(userId?: string): ThemePreference | undefined;
82
+ setThemePreference(value: ThemePreference, userId?: string): void;
61
83
  getLogLevelPreference(): LogLevelName | undefined;
62
84
  setLogLevelPreference(value: LogLevelName): void;
63
85
 
64
- listPasskeys(): StoredPasskey[];
86
+ countUsers(): number;
87
+ listUsers(): UserRecord[];
88
+ createUser(user: UserRecord): void;
89
+ getUserById(id: string): UserRecord | undefined;
90
+ getUserByUsername(username: string): UserRecord | undefined;
91
+ getOwnerUser(): UserRecord | undefined;
92
+ setUserRole(id: string, role: WebAppUserRole, updatedAt: string): boolean;
93
+ markUserLogin(id: string, lastLoginAt: string): void;
94
+ incrementUserAuthVersion(id: string, updatedAt: string): void;
95
+ deleteUser(id: string): boolean;
96
+
97
+ createSetupLink(record: UserSetupLinkRecord): void;
98
+ getSetupLinkByTokenHash(tokenHash: string): UserSetupLinkRecord | undefined;
99
+ consumeSetupLink(id: string, consumedAt: string): void;
100
+ deletePendingSetupLinksForUser(userId: string, nowIso: string): void;
101
+
102
+ saveAuditEvent(record: AuditEventRecord): void;
103
+ listAuditEvents(limit?: number): AuditEventRecord[];
104
+
105
+ listPasskeys(userId?: string): StoredPasskey[];
106
+ getPasskeyByUserId(userId: string): StoredPasskey | undefined;
65
107
  getPasskeyByCredentialId(credentialId: string): StoredPasskey | undefined;
66
108
  savePasskey(passkey: StoredPasskey): void;
67
109
  updatePasskeyUsage(credentialId: string, counter: number, lastUsedAt: string): void;
68
- deleteAllPasskeys(): void;
110
+ deletePasskeysForUser(userId: string): void;
69
111
 
70
- listApiKeys(): ApiKeyRecord[];
112
+ listApiKeys(userId?: string): ApiKeyRecord[];
71
113
  getApiKeyByHash(tokenHash: string): ApiKeyRecord | undefined;
72
114
  saveApiKey(record: ApiKeyRecord): void;
73
115
  touchApiKey(id: string, lastUsedAt: string): void;
74
- deleteApiKey(id: string): boolean;
116
+ deleteApiKey(id: string, userId?: string): boolean;
117
+ deleteApiKeysForUser(userId: string): void;
75
118
 
76
119
  saveDeviceAuthRequest(record: DeviceAuthRequestRecord): void;
77
120
  getDeviceAuthByUserCode(userCode: string): DeviceAuthRequestRecord | undefined;
78
121
  getDeviceAuthByDeviceCodeHash(deviceCodeHash: string): DeviceAuthRequestRecord | undefined;
79
- updateDeviceAuthStatus(userCode: string, status: DeviceAuthRequestRecord["status"], updatedAt: string): void;
122
+ updateDeviceAuthStatus(userCode: string, status: DeviceAuthRequestRecord["status"], updatedAt: string, approvedByUserId?: string): void;
80
123
  deleteExpiredDeviceAuthRequests(nowIso: string): void;
81
124
 
82
125
  getSigningKey(): SigningKeyRecord | undefined;
@@ -84,8 +127,9 @@ export interface WebAppStore {
84
127
 
85
128
  saveRefreshSession(record: RefreshSessionRecord): void;
86
129
  getRefreshSessionByHash(refreshTokenHash: string): RefreshSessionRecord | undefined;
87
- listRefreshSessions(): RefreshSessionRecord[];
130
+ listRefreshSessions(userId?: string): RefreshSessionRecord[];
88
131
  rotateRefreshSession(oldHash: string, next: RefreshSessionRecord, nowIso: string): RefreshSessionRecord | undefined;
89
- revokeRefreshSession(id: string, revokedAt: string): boolean;
132
+ revokeRefreshSession(id: string, revokedAt: string, userId?: string): boolean;
90
133
  revokeRefreshFamily(familyId: string, revokedAt: string): void;
134
+ revokeRefreshSessionsForUser(userId: string, revokedAt: string): void;
91
135
  }
@@ -1,5 +1,9 @@
1
+ import type { CurrentUser } from "../../contracts";
2
+
1
3
  export interface AccessTokenClaims {
2
4
  sub: string;
5
+ username?: string;
6
+ role?: string;
3
7
  jti: string;
4
8
  sid: string;
5
9
  clientId: string;
@@ -8,9 +12,9 @@ export interface AccessTokenClaims {
8
12
 
9
13
  export type AuthenticatedRequestState =
10
14
  | { kind: "anonymous" }
11
- | { kind: "passkey"; passkeyId?: string }
12
- | { kind: "api-key"; apiKeyId: string; scopes: string[] }
13
- | { kind: "bearer"; claims: AccessTokenClaims };
15
+ | { kind: "passkey"; user: CurrentUser; passkeyId?: string }
16
+ | { kind: "api-key"; user: CurrentUser; apiKeyId: string; scopes: string[] }
17
+ | { kind: "bearer"; user: CurrentUser; claims: AccessTokenClaims };
14
18
 
15
19
  export class AuthError extends Error {
16
20
  code: string;
@@ -0,0 +1,83 @@
1
+ import type { CurrentUser, WebAppUserRole, WebAppUserSummary } from "../../contracts";
2
+ import type { AuditEventRecord, UserRecord, WebAppStore } from "./store";
3
+ import { addSeconds, nowIso, sha256 } from "./crypto";
4
+ import { AuthError } from "./types";
5
+
6
+ export const USERNAME_PATTERN = /^[a-z0-9._-]{3,32}$/;
7
+ export const SETUP_LINK_TTL_SECONDS = 60 * 60 * 24;
8
+
9
+ export function normalizeUsername(username: string): string {
10
+ return username.trim().toLowerCase();
11
+ }
12
+
13
+ export function assertValidUsername(username: string): string {
14
+ const normalized = normalizeUsername(username);
15
+ if (!USERNAME_PATTERN.test(normalized)) {
16
+ throw new AuthError("invalid_username", "Username must be 3-32 lowercase letters, numbers, dots, underscores or hyphens", 400);
17
+ }
18
+ return normalized;
19
+ }
20
+
21
+ export function toCurrentUser(user: UserRecord): CurrentUser {
22
+ return {
23
+ id: user.id,
24
+ username: user.username,
25
+ role: user.role,
26
+ isOwner: user.role === "owner",
27
+ isAdmin: user.role === "owner" || user.role === "admin",
28
+ };
29
+ }
30
+
31
+ export function summarizeUser(user: UserRecord): WebAppUserSummary {
32
+ return {
33
+ id: user.id,
34
+ username: user.username,
35
+ role: user.role,
36
+ passkeyConfigured: user.passkeyConfigured,
37
+ createdAt: user.createdAt,
38
+ updatedAt: user.updatedAt,
39
+ lastLoginAt: user.lastLoginAt,
40
+ };
41
+ }
42
+
43
+ export function createUserRecord(input: { username: string; role: WebAppUserRole }): UserRecord {
44
+ const createdAt = nowIso();
45
+ return {
46
+ id: crypto.randomUUID(),
47
+ username: assertValidUsername(input.username),
48
+ role: input.role,
49
+ authVersion: 1,
50
+ passkeyConfigured: false,
51
+ createdAt,
52
+ updatedAt: createdAt,
53
+ };
54
+ }
55
+
56
+ export function createSetupLinkRecord(input: {
57
+ userId: string;
58
+ token: string;
59
+ kind: "invite" | "reset" | "owner-reset";
60
+ createdByUserId?: string;
61
+ }) {
62
+ const createdAt = nowIso();
63
+ return {
64
+ id: crypto.randomUUID(),
65
+ userId: input.userId,
66
+ tokenHash: sha256(input.token),
67
+ kind: input.kind,
68
+ createdByUserId: input.createdByUserId,
69
+ createdAt,
70
+ expiresAt: addSeconds(SETUP_LINK_TTL_SECONDS),
71
+ };
72
+ }
73
+
74
+ export function audit(store: WebAppStore, input: Omit<AuditEventRecord, "id" | "createdAt" | "metadata"> & { metadata?: Record<string, unknown> }): void {
75
+ store.saveAuditEvent({
76
+ id: crypto.randomUUID(),
77
+ eventType: input.eventType,
78
+ actorUserId: input.actorUserId,
79
+ targetUserId: input.targetUserId,
80
+ metadata: input.metadata ?? {},
81
+ createdAt: nowIso(),
82
+ });
83
+ }