@structured-id/ui-core 0.1.5

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.
Files changed (156) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +47 -0
  3. package/dist/browser-vp/index.cjs +349 -0
  4. package/dist/browser-vp/index.d.cts +287 -0
  5. package/dist/browser-vp/index.d.ts +287 -0
  6. package/dist/browser-vp/index.js +349 -0
  7. package/dist/chunk-C3Q766CY.cjs +125 -0
  8. package/dist/chunk-J5YXRXB5.js +125 -0
  9. package/dist/index.cjs +19360 -0
  10. package/dist/index.d.cts +9536 -0
  11. package/dist/index.d.ts +9536 -0
  12. package/dist/index.js +19360 -0
  13. package/dist/storage-4CB2JTLW.cjs +22 -0
  14. package/dist/storage-THRWREZP.js +22 -0
  15. package/package.json +93 -0
  16. package/src/browser-vp/crypto.ts +254 -0
  17. package/src/browser-vp/index.test.ts +10 -0
  18. package/src/browser-vp/index.ts +118 -0
  19. package/src/browser-vp/prf.ts +181 -0
  20. package/src/browser-vp/storage.test.ts +9 -0
  21. package/src/browser-vp/storage.ts +165 -0
  22. package/src/browser-vp/types.ts +97 -0
  23. package/src/browser-vp/vp.test.ts +43 -0
  24. package/src/browser-vp/vp.ts +113 -0
  25. package/src/components/SidAvatar.vue +106 -0
  26. package/src/components/SidConfirmDialog.vue +125 -0
  27. package/src/components/SidStatusBadge.vue +71 -0
  28. package/src/components/index.ts +5 -0
  29. package/src/composables/index.ts +10 -0
  30. package/src/composables/useAuth.ts +40 -0
  31. package/src/composables/useBffAuth.ts +147 -0
  32. package/src/composables/useGrpc.test.ts +196 -0
  33. package/src/composables/useGrpc.ts +120 -0
  34. package/src/composables/useSession.ts +80 -0
  35. package/src/generated/google/api/http.ts +687 -0
  36. package/src/generated/google/protobuf/descriptor.ts +4865 -0
  37. package/src/generated/google/protobuf/duration.ts +230 -0
  38. package/src/generated/google/protobuf/empty.ts +94 -0
  39. package/src/generated/google/protobuf/struct.ts +484 -0
  40. package/src/generated/google/protobuf/timestamp.ts +287 -0
  41. package/src/generated/sid/v1/account/account.client.ts +527 -0
  42. package/src/generated/sid/v1/account/account.ts +3261 -0
  43. package/src/generated/sid/v1/admin/admin.client.ts +136 -0
  44. package/src/generated/sid/v1/admin/admin.ts +846 -0
  45. package/src/generated/sid/v1/admin/admin_roles.client.ts +181 -0
  46. package/src/generated/sid/v1/admin/admin_roles.ts +1606 -0
  47. package/src/generated/sid/v1/admin/analytics.client.ts +106 -0
  48. package/src/generated/sid/v1/admin/analytics.ts +1017 -0
  49. package/src/generated/sid/v1/admin/branding.client.ts +166 -0
  50. package/src/generated/sid/v1/admin/branding.ts +1906 -0
  51. package/src/generated/sid/v1/admin/enrollment.client.ts +165 -0
  52. package/src/generated/sid/v1/admin/enrollment.ts +1704 -0
  53. package/src/generated/sid/v1/admin/events.client.ts +195 -0
  54. package/src/generated/sid/v1/admin/events.ts +1900 -0
  55. package/src/generated/sid/v1/admin/flow.client.ts +183 -0
  56. package/src/generated/sid/v1/admin/flow.ts +1552 -0
  57. package/src/generated/sid/v1/admin/keys.client.ts +107 -0
  58. package/src/generated/sid/v1/admin/keys.ts +931 -0
  59. package/src/generated/sid/v1/admin/login_experience.client.ts +160 -0
  60. package/src/generated/sid/v1/admin/login_experience.ts +1061 -0
  61. package/src/generated/sid/v1/admin/migration.client.ts +127 -0
  62. package/src/generated/sid/v1/admin/migration.ts +1948 -0
  63. package/src/generated/sid/v1/admin/notifications.client.ts +128 -0
  64. package/src/generated/sid/v1/admin/notifications.ts +1332 -0
  65. package/src/generated/sid/v1/admin/operations.client.ts +241 -0
  66. package/src/generated/sid/v1/admin/operations.ts +2648 -0
  67. package/src/generated/sid/v1/admin/organizations.client.ts +57 -0
  68. package/src/generated/sid/v1/admin/organizations.ts +349 -0
  69. package/src/generated/sid/v1/admin/scopes.client.ts +171 -0
  70. package/src/generated/sid/v1/admin/scopes.ts +1324 -0
  71. package/src/generated/sid/v1/admin/security.client.ts +412 -0
  72. package/src/generated/sid/v1/admin/security.ts +4086 -0
  73. package/src/generated/sid/v1/admin/sessions.client.ts +114 -0
  74. package/src/generated/sid/v1/admin/sessions.ts +923 -0
  75. package/src/generated/sid/v1/admin/settings.client.ts +340 -0
  76. package/src/generated/sid/v1/admin/settings.ts +2395 -0
  77. package/src/generated/sid/v1/admin/user_profile.client.ts +57 -0
  78. package/src/generated/sid/v1/admin/user_profile.ts +730 -0
  79. package/src/generated/sid/v1/admin/webhooks.client.ts +269 -0
  80. package/src/generated/sid/v1/admin/webhooks.ts +2804 -0
  81. package/src/generated/sid/v1/attestation/attestation.client.ts +115 -0
  82. package/src/generated/sid/v1/attestation/attestation.ts +813 -0
  83. package/src/generated/sid/v1/authn/auth.client.ts +900 -0
  84. package/src/generated/sid/v1/authn/auth.ts +9287 -0
  85. package/src/generated/sid/v1/authn/flows.client.ts +413 -0
  86. package/src/generated/sid/v1/authn/flows.ts +3217 -0
  87. package/src/generated/sid/v1/authz/authz.client.ts +459 -0
  88. package/src/generated/sid/v1/authz/authz.ts +5343 -0
  89. package/src/generated/sid/v1/authz/governance.client.ts +102 -0
  90. package/src/generated/sid/v1/authz/governance.ts +818 -0
  91. package/src/generated/sid/v1/common/errors.ts +184 -0
  92. package/src/generated/sid/v1/common/pagination.ts +152 -0
  93. package/src/generated/sid/v1/common/transport.ts +227 -0
  94. package/src/generated/sid/v1/common/types.ts +151 -0
  95. package/src/generated/sid/v1/events/event_stream.client.ts +67 -0
  96. package/src/generated/sid/v1/events/event_stream.ts +374 -0
  97. package/src/generated/sid/v1/federation/upstream.client.ts +142 -0
  98. package/src/generated/sid/v1/federation/upstream.ts +1736 -0
  99. package/src/generated/sid/v1/identity/identity.client.ts +467 -0
  100. package/src/generated/sid/v1/identity/identity.ts +5613 -0
  101. package/src/generated/sid/v1/machine/machine_user.client.ts +219 -0
  102. package/src/generated/sid/v1/machine/machine_user.ts +2400 -0
  103. package/src/generated/sid/v1/machine/pat.client.ts +147 -0
  104. package/src/generated/sid/v1/machine/pat.ts +1268 -0
  105. package/src/generated/sid/v1/projects/projects.client.ts +286 -0
  106. package/src/generated/sid/v1/projects/projects.ts +2714 -0
  107. package/src/generated/sid/v1/scim/scim.client.ts +274 -0
  108. package/src/generated/sid/v1/scim/scim.ts +3043 -0
  109. package/src/generated/sid/v1/test/test.client.ts +232 -0
  110. package/src/generated/sid/v1/test/test.ts +1136 -0
  111. package/src/i18n/index.ts +40 -0
  112. package/src/index.ts +157 -0
  113. package/src/profile/auth.ts +15 -0
  114. package/src/profile/components/ConsentManager.vue +88 -0
  115. package/src/profile/components/ProfileEditor.vue +146 -0
  116. package/src/profile/components/SecuritySettings.vue +170 -0
  117. package/src/profile/components/SessionList.vue +100 -0
  118. package/src/profile/components/SidLoginForm.test.ts +80 -0
  119. package/src/profile/components/SidLoginForm.vue +230 -0
  120. package/src/profile/components/SidMfaChallenge.test.ts +127 -0
  121. package/src/profile/components/SidMfaChallenge.vue +243 -0
  122. package/src/profile/components/SidRegistrationForm.test.ts +79 -0
  123. package/src/profile/components/SidRegistrationForm.vue +317 -0
  124. package/src/profile/components/index.ts +11 -0
  125. package/src/profile/composables/index.ts +6 -0
  126. package/src/profile/composables/useAccountApi.ts +203 -0
  127. package/src/profile/composables/useAuthApi.opaque.test.ts +140 -0
  128. package/src/profile/composables/useAuthApi.ts +617 -0
  129. package/src/profile/composables/useIdentityApi.ts +123 -0
  130. package/src/profile/composables/useLoginSuccess.test.ts +112 -0
  131. package/src/profile/composables/useLoginSuccess.ts +65 -0
  132. package/src/profile/composables/useOpaqueAuth.test.ts +290 -0
  133. package/src/profile/composables/useOpaqueAuth.ts +191 -0
  134. package/src/profile/composables/useSecurityLevel.test.ts +198 -0
  135. package/src/profile/composables/useSecurityLevel.ts +324 -0
  136. package/src/profile/index.ts +18 -0
  137. package/src/quasar/SidPrincipalInput.vue +209 -0
  138. package/src/quasar/index.ts +8 -0
  139. package/src/stores/auth.test.ts +83 -0
  140. package/src/stores/auth.ts +92 -0
  141. package/src/stores/index.ts +1 -0
  142. package/src/transport/adaptive.test.ts +728 -0
  143. package/src/transport/adaptive.ts +308 -0
  144. package/src/transport/framing.test.ts +227 -0
  145. package/src/transport/framing.ts +98 -0
  146. package/src/transport/webtransport.test.ts +728 -0
  147. package/src/transport/webtransport.ts +323 -0
  148. package/src/types/auth.ts +58 -0
  149. package/src/types/index.ts +1 -0
  150. package/src/utils/formatters.test.ts +26 -0
  151. package/src/utils/formatters.ts +69 -0
  152. package/src/utils/index.ts +16 -0
  153. package/src/utils/normalizePrincipal.test.ts +188 -0
  154. package/src/utils/normalizePrincipal.ts +159 -0
  155. package/src/utils/validators.test.ts +75 -0
  156. package/src/utils/validators.ts +36 -0
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { isIndexedDbAvailable } from "./storage";
3
+
4
+ describe("isIndexedDbAvailable", () => {
5
+ it("returns false in Node.js environment (no indexedDB global)", () => {
6
+ // Node.js doesn't have indexedDB
7
+ expect(isIndexedDbAvailable()).toBe(false);
8
+ });
9
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * IndexedDB storage for Browser VP.
3
+ *
4
+ * Database: sid-browser-vp
5
+ * ├── bindings — per-service binding entries (key: service_sector)
6
+ * └── profile-state — profile state + encrypted blob (key: profile_id)
7
+ *
8
+ * See arch/auth/browser-vp.md § IndexedDB Storage Schema.
9
+ */
10
+
11
+ import type { ProfileState, BindingEntry } from "./types";
12
+
13
+ const DB_NAME = "sid-browser-vp";
14
+ const DB_VERSION = 1;
15
+ const STORE_BINDINGS = "bindings";
16
+ const STORE_PROFILE = "profile-state";
17
+
18
+ /**
19
+ * Open (or create) the IndexedDB database.
20
+ */
21
+ function openDb(): Promise<IDBDatabase> {
22
+ return new Promise((resolve, reject) => {
23
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
24
+
25
+ request.onupgradeneeded = () => {
26
+ const db = request.result;
27
+ if (!db.objectStoreNames.contains(STORE_BINDINGS)) {
28
+ db.createObjectStore(STORE_BINDINGS, { keyPath: "serviceSector" });
29
+ }
30
+ if (!db.objectStoreNames.contains(STORE_PROFILE)) {
31
+ db.createObjectStore(STORE_PROFILE, { keyPath: "profileId" });
32
+ }
33
+ };
34
+
35
+ request.onsuccess = () => resolve(request.result);
36
+ request.onerror = () => reject(request.error);
37
+ });
38
+ }
39
+
40
+ // ── Profile State ──
41
+
42
+ /** Save or update profile state. */
43
+ export async function saveProfileState(
44
+ profileId: string,
45
+ state: ProfileState,
46
+ ): Promise<void> {
47
+ const db = await openDb();
48
+ return new Promise((resolve, reject) => {
49
+ const tx = db.transaction(STORE_PROFILE, "readwrite");
50
+ tx.objectStore(STORE_PROFILE).put({ profileId, ...state });
51
+ tx.oncomplete = () => resolve();
52
+ tx.onerror = () => reject(tx.error);
53
+ });
54
+ }
55
+
56
+ /** Load profile state by ID. Returns null if not found. */
57
+ export async function loadProfileState(
58
+ profileId: string,
59
+ ): Promise<ProfileState | null> {
60
+ const db = await openDb();
61
+ return new Promise((resolve, reject) => {
62
+ const tx = db.transaction(STORE_PROFILE, "readonly");
63
+ const request = tx.objectStore(STORE_PROFILE).get(profileId);
64
+ request.onsuccess = () => {
65
+ const result = request.result;
66
+ if (!result) {
67
+ resolve(null);
68
+ return;
69
+ }
70
+ // Strip the profileId key we added for IndexedDB
71
+ const { profileId: _profileId, ...state } = result;
72
+ resolve(state as ProfileState);
73
+ };
74
+ request.onerror = () => reject(request.error);
75
+ });
76
+ }
77
+
78
+ /** Delete profile state. */
79
+ export async function deleteProfileState(profileId: string): Promise<void> {
80
+ const db = await openDb();
81
+ return new Promise((resolve, reject) => {
82
+ const tx = db.transaction(STORE_PROFILE, "readwrite");
83
+ tx.objectStore(STORE_PROFILE).delete(profileId);
84
+ tx.oncomplete = () => resolve();
85
+ tx.onerror = () => reject(tx.error);
86
+ });
87
+ }
88
+
89
+ // ── Bindings ──
90
+
91
+ /** Save or update a binding entry. */
92
+ export async function saveBinding(
93
+ serviceSector: string,
94
+ entry: BindingEntry,
95
+ ): Promise<void> {
96
+ const db = await openDb();
97
+ return new Promise((resolve, reject) => {
98
+ const tx = db.transaction(STORE_BINDINGS, "readwrite");
99
+ tx.objectStore(STORE_BINDINGS).put({ serviceSector, ...entry });
100
+ tx.oncomplete = () => resolve();
101
+ tx.onerror = () => reject(tx.error);
102
+ });
103
+ }
104
+
105
+ /** Load a binding by service sector. */
106
+ export async function loadBinding(
107
+ serviceSector: string,
108
+ ): Promise<BindingEntry | null> {
109
+ const db = await openDb();
110
+ return new Promise((resolve, reject) => {
111
+ const tx = db.transaction(STORE_BINDINGS, "readonly");
112
+ const request = tx.objectStore(STORE_BINDINGS).get(serviceSector);
113
+ request.onsuccess = () => {
114
+ const result = request.result;
115
+ if (!result) {
116
+ resolve(null);
117
+ return;
118
+ }
119
+ const { serviceSector: _serviceSector, ...entry } = result;
120
+ resolve(entry as BindingEntry);
121
+ };
122
+ request.onerror = () => reject(request.error);
123
+ });
124
+ }
125
+
126
+ /** List all stored bindings. */
127
+ export async function listBindings(): Promise<
128
+ Array<{ serviceSector: string } & BindingEntry>
129
+ > {
130
+ const db = await openDb();
131
+ return new Promise((resolve, reject) => {
132
+ const tx = db.transaction(STORE_BINDINGS, "readonly");
133
+ const request = tx.objectStore(STORE_BINDINGS).getAll();
134
+ request.onsuccess = () => resolve(request.result ?? []);
135
+ request.onerror = () => reject(request.error);
136
+ });
137
+ }
138
+
139
+ /** Delete a binding. */
140
+ export async function deleteBinding(serviceSector: string): Promise<void> {
141
+ const db = await openDb();
142
+ return new Promise((resolve, reject) => {
143
+ const tx = db.transaction(STORE_BINDINGS, "readwrite");
144
+ tx.objectStore(STORE_BINDINGS).delete(serviceSector);
145
+ tx.oncomplete = () => resolve();
146
+ tx.onerror = () => reject(tx.error);
147
+ });
148
+ }
149
+
150
+ /** Clear all data (profile state + bindings). */
151
+ export async function clearAll(): Promise<void> {
152
+ const db = await openDb();
153
+ return new Promise((resolve, reject) => {
154
+ const tx = db.transaction([STORE_PROFILE, STORE_BINDINGS], "readwrite");
155
+ tx.objectStore(STORE_PROFILE).clear();
156
+ tx.objectStore(STORE_BINDINGS).clear();
157
+ tx.oncomplete = () => resolve();
158
+ tx.onerror = () => reject(tx.error);
159
+ });
160
+ }
161
+
162
+ /** Check if IndexedDB is available in this environment. */
163
+ export function isIndexedDbAvailable(): boolean {
164
+ return typeof indexedDB !== "undefined";
165
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Browser VP types.
3
+ *
4
+ * See arch/auth/browser-vp.md for full specification.
5
+ */
6
+
7
+ /** VP strategy — automatically selected based on browser capabilities. */
8
+ export type VpStrategy = "prf" | "device-key";
9
+
10
+ /** Profile state stored in IndexedDB. */
11
+ export interface ProfileState {
12
+ /** Which strategy this profile uses. */
13
+ strategy: VpStrategy;
14
+
15
+ /** Encrypted profile blob (< 1MB typical). */
16
+ encryptedBlob: ArrayBuffer;
17
+
18
+ /** Optimistic sync version with SID server. */
19
+ blobVersion: number;
20
+
21
+ // Strategy A (PRF) fields
22
+ /** PRF salt identifier (for versioning). */
23
+ prfSalt?: string;
24
+ /** SHA-256 of master public key (authenticator change detection). */
25
+ masterPublicKeyHash?: string;
26
+
27
+ // Strategy B (device-key) fields
28
+ /** Non-extractable CryptoKey reference (browser-managed). */
29
+ cryptoKeyRef?: CryptoKey;
30
+
31
+ /** Last sync timestamp with SID server. */
32
+ lastSyncedAt: Date;
33
+ /** SID device ID for this browser instance. */
34
+ deviceId: string;
35
+ }
36
+
37
+ /** Per-service binding stored in IndexedDB. */
38
+ export interface BindingEntry {
39
+ /** Opaque BindingId (pairwise per-service). */
40
+ bindingId: string;
41
+ /** HD derivation index for this binding. */
42
+ bindingIndex: number;
43
+ /** Per-binding certificate chain (DER-encoded). */
44
+ certChain: ArrayBuffer;
45
+ /** Certificate expiry date. */
46
+ certExpiry: Date;
47
+ /** Last time a VP was created for this binding. */
48
+ lastUsedAt: Date;
49
+ }
50
+
51
+ /** Key material derived from PRF or generated via WebCrypto. */
52
+ export interface DerivedKeys {
53
+ /** ECDSA P-256 signing key (non-extractable). */
54
+ signingKey: CryptoKeyPair;
55
+ /** AES-256-GCM data encryption key. */
56
+ dataKey: CryptoKey;
57
+ }
58
+
59
+ /** Verifiable Presentation payload. */
60
+ export interface VerifiablePresentation {
61
+ /** The service sector this VP is bound to. */
62
+ serviceSector: string;
63
+ /** Binding ID (pairwise). */
64
+ bindingId: string;
65
+ /** Signed VP payload (CBOR or JSON). */
66
+ payload: ArrayBuffer;
67
+ /** Signature over payload. */
68
+ signature: ArrayBuffer;
69
+ /** Certificate chain for verification. */
70
+ certChain: ArrayBuffer;
71
+ /** Nonce from relying party (replay protection). */
72
+ nonce: string;
73
+ /** Audience (relying party origin). */
74
+ audience: string;
75
+ }
76
+
77
+ /** Options for VP creation request from a relying party. */
78
+ export interface VpRequest {
79
+ /** Nonce for replay protection. */
80
+ nonce: string;
81
+ /** Requested claims (e.g., ["email", "name"]). */
82
+ requestedClaims: string[];
83
+ /** Audience (relying party origin). */
84
+ audience: string;
85
+ /** Service sector identifier. */
86
+ serviceSector: string;
87
+ }
88
+
89
+ /** Result of browser VP initialization. */
90
+ export interface BrowserVpContext {
91
+ /** Active strategy. */
92
+ strategy: VpStrategy;
93
+ /** Profile ID. */
94
+ profileId: string;
95
+ /** Whether VP creation is available (keys + blob + cert ready). */
96
+ ready: boolean;
97
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { needsRenewal } from "./vp";
3
+ import type { BindingEntry } from "./types";
4
+
5
+ function makeBinding(certExpiry: Date): BindingEntry {
6
+ return {
7
+ bindingId: "bind_test",
8
+ bindingIndex: 0,
9
+ certChain: new ArrayBuffer(0),
10
+ certExpiry,
11
+ lastUsedAt: new Date(),
12
+ };
13
+ }
14
+
15
+ describe("needsRenewal", () => {
16
+ it("returns true when cert expires in less than 7 days", () => {
17
+ const sixDays = Date.now() + 6 * 24 * 60 * 60 * 1000;
18
+ expect(needsRenewal(makeBinding(new Date(sixDays)))).toBe(true);
19
+ });
20
+
21
+ it("returns true when cert is already expired", () => {
22
+ const yesterday = Date.now() - 24 * 60 * 60 * 1000;
23
+ expect(needsRenewal(makeBinding(new Date(yesterday)))).toBe(true);
24
+ });
25
+
26
+ it("returns false when cert expires in more than 7 days", () => {
27
+ const eightDays = Date.now() + 8 * 24 * 60 * 60 * 1000;
28
+ expect(needsRenewal(makeBinding(new Date(eightDays)))).toBe(false);
29
+ });
30
+
31
+ it("returns true at exactly 7 days boundary", () => {
32
+ // At exactly 7 days, difference < 7 days in ms because of execution time
33
+ // At exactly 7 days, could be true or false depending on execution timing.
34
+ // We test just under 7 days to be deterministic.
35
+ const justUnder = Date.now() + 7 * 24 * 60 * 60 * 1000 - 1000;
36
+ expect(needsRenewal(makeBinding(new Date(justUnder)))).toBe(true);
37
+ });
38
+
39
+ it("returns false for far-future expiry", () => {
40
+ const oneYear = Date.now() + 365 * 24 * 60 * 60 * 1000;
41
+ expect(needsRenewal(makeBinding(new Date(oneYear)))).toBe(false);
42
+ });
43
+ });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Verifiable Presentation creation.
3
+ *
4
+ * Builds and signs VPs using cached keys and profile blob.
5
+ * VP creation is fully offline (~50ms with biometric cache).
6
+ *
7
+ * See arch/auth/browser-vp.md § VP Creation.
8
+ */
9
+
10
+ import type { VpRequest, VerifiablePresentation, BindingEntry } from "./types";
11
+ import { sign } from "./crypto";
12
+ import { loadBinding, saveBinding } from "./storage";
13
+
14
+ const encoder = new TextEncoder();
15
+
16
+ /**
17
+ * Create a Verifiable Presentation for a service.
18
+ *
19
+ * @param request - VP request from the relying party.
20
+ * @param signingKey - ECDSA P-256 private key (master or per-binding).
21
+ * @param profileBlob - Decrypted profile blob.
22
+ * @returns Signed VP, or null if binding not found.
23
+ */
24
+ export async function createVp(
25
+ request: VpRequest,
26
+ signingKey: CryptoKey,
27
+ profileBlob: ArrayBuffer,
28
+ ): Promise<VerifiablePresentation | null> {
29
+ // Load binding for this service sector
30
+ const binding = await loadBinding(request.serviceSector);
31
+ if (!binding) {
32
+ return null;
33
+ }
34
+
35
+ // Check certificate expiry
36
+ if (binding.certExpiry < new Date()) {
37
+ // Certificate expired — VP creation blocked
38
+ // Caller should trigger online renewal
39
+ return null;
40
+ }
41
+
42
+ // Build VP payload
43
+ const payload = buildPayload(request, binding, profileBlob);
44
+
45
+ // Sign with binding key
46
+ const signature = await sign(payload, signingKey);
47
+
48
+ // Update last used timestamp
49
+ await saveBinding(request.serviceSector, {
50
+ ...binding,
51
+ lastUsedAt: new Date(),
52
+ });
53
+
54
+ return {
55
+ serviceSector: request.serviceSector,
56
+ bindingId: binding.bindingId,
57
+ payload,
58
+ signature,
59
+ certChain: binding.certChain,
60
+ nonce: request.nonce,
61
+ audience: request.audience,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Build the VP payload to be signed.
67
+ *
68
+ * The payload contains:
69
+ * - nonce (replay protection)
70
+ * - audience (relying party)
71
+ * - bindingId (pairwise identifier)
72
+ * - requested claims (selective disclosure)
73
+ * - timestamp
74
+ */
75
+ function buildPayload(
76
+ request: VpRequest,
77
+ binding: BindingEntry,
78
+ _profileBlob: ArrayBuffer,
79
+ ): ArrayBuffer {
80
+ // Simplified payload structure.
81
+ // Production: SD-JWT or Merkle-based selective disclosure
82
+ // from the profile blob.
83
+ const payload = {
84
+ nonce: request.nonce,
85
+ aud: request.audience,
86
+ sub: binding.bindingId,
87
+ iat: Math.floor(Date.now() / 1000),
88
+ claims: request.requestedClaims,
89
+ };
90
+
91
+ return encoder.encode(JSON.stringify(payload)).buffer as ArrayBuffer;
92
+ }
93
+
94
+ /**
95
+ * Check if a binding's certificate needs renewal.
96
+ *
97
+ * Renewal is attempted 7 days before expiry (background).
98
+ */
99
+ export function needsRenewal(binding: BindingEntry): boolean {
100
+ const sevenDays = 7 * 24 * 60 * 60 * 1000;
101
+ return binding.certExpiry.getTime() - Date.now() < sevenDays;
102
+ }
103
+
104
+ /**
105
+ * List all bindings that need certificate renewal.
106
+ */
107
+ export async function getExpiringSoon(): Promise<
108
+ Array<{ serviceSector: string } & BindingEntry>
109
+ > {
110
+ const { listBindings } = await import("./storage");
111
+ const all = await listBindings();
112
+ return all.filter((b) => needsRenewal(b));
113
+ }
@@ -0,0 +1,106 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Avatar component with image, initials fallback, and size variants.
4
+ *
5
+ * Pure Vue 3, no Quasar dependency. Consuming apps can override
6
+ * styles via CSS custom properties.
7
+ */
8
+ import { computed } from "vue";
9
+
10
+ export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
11
+
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ /** Image URL (optional — falls back to initials). */
15
+ src?: string | null;
16
+ /** Display name for initials fallback. */
17
+ name?: string;
18
+ /** Size variant. */
19
+ size?: AvatarSize;
20
+ }>(),
21
+ {
22
+ src: null,
23
+ name: "",
24
+ size: "md",
25
+ },
26
+ );
27
+
28
+ const sizeMap: Record<AvatarSize, number> = {
29
+ xs: 24,
30
+ sm: 32,
31
+ md: 40,
32
+ lg: 56,
33
+ xl: 80,
34
+ };
35
+
36
+ const fontSizeMap: Record<AvatarSize, string> = {
37
+ xs: "0.625rem",
38
+ sm: "0.75rem",
39
+ md: "0.875rem",
40
+ lg: "1.25rem",
41
+ xl: "1.75rem",
42
+ };
43
+
44
+ const initials = computed(() => {
45
+ if (!props.name) return "?";
46
+ const parts = props.name.trim().split(/\s+/);
47
+ if (parts.length === 1) return parts[0][0].toUpperCase();
48
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
49
+ });
50
+
51
+ /** Deterministic color from name string. */
52
+ const bgColor = computed(() => {
53
+ if (!props.name) return "var(--sid-avatar-bg, #9e9e9e)";
54
+ let hash = 0;
55
+ for (const ch of props.name) {
56
+ hash = ch.charCodeAt(0) + ((hash << 5) - hash);
57
+ }
58
+ const hue = Math.abs(hash) % 360;
59
+ return `hsl(${hue}, 45%, 55%)`;
60
+ });
61
+
62
+ const dimension = computed(() => `${sizeMap[props.size]}px`);
63
+ const fontSize = computed(() => fontSizeMap[props.size]);
64
+ </script>
65
+
66
+ <template>
67
+ <span
68
+ class="sid-avatar"
69
+ :style="{
70
+ width: dimension,
71
+ height: dimension,
72
+ fontSize: fontSize,
73
+ backgroundColor: src ? 'transparent' : bgColor,
74
+ }"
75
+ >
76
+ <img
77
+ v-if="src"
78
+ :src="src"
79
+ :alt="name || 'Avatar'"
80
+ class="sid-avatar__img"
81
+ />
82
+ <span v-else class="sid-avatar__initials">{{ initials }}</span>
83
+ </span>
84
+ </template>
85
+
86
+ <style scoped>
87
+ .sid-avatar {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ border-radius: 50%;
92
+ overflow: hidden;
93
+ flex-shrink: 0;
94
+ user-select: none;
95
+ }
96
+ .sid-avatar__img {
97
+ width: 100%;
98
+ height: 100%;
99
+ object-fit: cover;
100
+ }
101
+ .sid-avatar__initials {
102
+ color: var(--sid-avatar-text, #fff);
103
+ font-weight: 500;
104
+ line-height: 1;
105
+ }
106
+ </style>
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Confirmation dialog component (pure Vue 3, no Quasar).
4
+ *
5
+ * Renders a modal overlay with confirm/cancel actions.
6
+ * Consuming apps can style via CSS custom properties or
7
+ * replace entirely with their own Quasar q-dialog wrapper.
8
+ */
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ modelValue: boolean;
12
+ title: string;
13
+ message: string;
14
+ confirmLabel?: string;
15
+ cancelLabel?: string;
16
+ destructive?: boolean;
17
+ }>(),
18
+ {
19
+ confirmLabel: "Confirm",
20
+ cancelLabel: "Cancel",
21
+ destructive: false,
22
+ },
23
+ );
24
+
25
+ const emit = defineEmits<{
26
+ "update:modelValue": [value: boolean];
27
+ confirm: [];
28
+ cancel: [];
29
+ }>();
30
+
31
+ function close() {
32
+ emit("update:modelValue", false);
33
+ emit("cancel");
34
+ }
35
+
36
+ function confirm() {
37
+ emit("update:modelValue", false);
38
+ emit("confirm");
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <Teleport to="body">
44
+ <div v-if="props.modelValue" class="sid-dialog-overlay" @click.self="close">
45
+ <div class="sid-dialog" role="dialog" aria-modal="true">
46
+ <h3 class="sid-dialog__title">{{ props.title }}</h3>
47
+ <p class="sid-dialog__message">{{ props.message }}</p>
48
+ <div class="sid-dialog__actions">
49
+ <button
50
+ class="sid-dialog__btn sid-dialog__btn--cancel"
51
+ @click="close"
52
+ >
53
+ {{ props.cancelLabel }}
54
+ </button>
55
+ <button
56
+ class="sid-dialog__btn"
57
+ :class="
58
+ props.destructive
59
+ ? 'sid-dialog__btn--destructive'
60
+ : 'sid-dialog__btn--confirm'
61
+ "
62
+ @click="confirm"
63
+ >
64
+ {{ props.confirmLabel }}
65
+ </button>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </Teleport>
70
+ </template>
71
+
72
+ <style scoped>
73
+ .sid-dialog-overlay {
74
+ position: fixed;
75
+ inset: 0;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ background: rgba(0, 0, 0, 0.5);
80
+ z-index: 9999;
81
+ }
82
+ .sid-dialog {
83
+ background: var(--sid-dialog-bg, #fff);
84
+ border-radius: 8px;
85
+ padding: 24px;
86
+ min-width: 320px;
87
+ max-width: 480px;
88
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
89
+ }
90
+ .sid-dialog__title {
91
+ margin: 0 0 8px;
92
+ font-size: 1.125rem;
93
+ font-weight: 600;
94
+ }
95
+ .sid-dialog__message {
96
+ margin: 0 0 24px;
97
+ color: var(--sid-dialog-text, #666);
98
+ line-height: 1.5;
99
+ }
100
+ .sid-dialog__actions {
101
+ display: flex;
102
+ justify-content: flex-end;
103
+ gap: 8px;
104
+ }
105
+ .sid-dialog__btn {
106
+ padding: 8px 16px;
107
+ border: none;
108
+ border-radius: 4px;
109
+ font-size: 0.875rem;
110
+ font-weight: 500;
111
+ cursor: pointer;
112
+ }
113
+ .sid-dialog__btn--cancel {
114
+ background: var(--sid-btn-cancel-bg, #f5f5f5);
115
+ color: var(--sid-btn-cancel-color, #333);
116
+ }
117
+ .sid-dialog__btn--confirm {
118
+ background: var(--sid-btn-confirm-bg, #1976d2);
119
+ color: var(--sid-btn-confirm-color, #fff);
120
+ }
121
+ .sid-dialog__btn--destructive {
122
+ background: var(--sid-btn-destructive-bg, #d32f2f);
123
+ color: var(--sid-btn-destructive-color, #fff);
124
+ }
125
+ </style>