@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,159 @@
1
+ import parsePhoneNumberFromString, {
2
+ isValidPhoneNumber,
3
+ } from "libphonenumber-js";
4
+
5
+ export type LoginInputType = "email" | "phone" | "username" | "unknown";
6
+
7
+ export interface NormalizedPrincipal {
8
+ /** Normalized value for storage, uniqueness check, and login lookup. */
9
+ value: string;
10
+ /** Original display form (mixed case preserved for usernames). */
11
+ display: string;
12
+ type: LoginInputType;
13
+ }
14
+
15
+ /**
16
+ * Global username rules per arch/identity/identity-model.md:
17
+ * Display: ^[a-zA-Z0-9_]{3,32}$ — user's original input, mixed case preserved
18
+ * Normalized: ^[a-z0-9_]{3,32}$ — lowercase, for storage + uniqueness + login
19
+ * - cannot start or end with underscore
20
+ * - no consecutive underscores (__)
21
+ *
22
+ * For detection (prematch) we use a relaxed version:
23
+ * - accepts minLength (3 for login, 6 for registration)
24
+ * - case-insensitive (will be lowercased on normalization)
25
+ */
26
+ const USERNAME_MAX_LENGTH = 32;
27
+
28
+ /**
29
+ * Detect principal type from user input.
30
+ *
31
+ * Priority: phone (+...) > email (@) > username (alphanumeric).
32
+ * Returns "unknown" if input doesn't match any pattern.
33
+ *
34
+ * @param minUsernameLength - minimum chars to detect as username (3 for login, 6 for register)
35
+ */
36
+ export function detectLoginInputType(
37
+ input: string,
38
+ minUsernameLength = 3,
39
+ ): LoginInputType {
40
+ const trimmed = input.trim();
41
+ if (!trimmed) return "unknown";
42
+
43
+ if (trimmed.startsWith("+")) return "phone";
44
+ if (trimmed.includes("@")) return "email";
45
+
46
+ // Federated username: login#domain (e.g., alice#acme.corp)
47
+ // No minUsernameLength — org admin decides login part length
48
+ if (trimmed.includes("#")) {
49
+ const hashIdx = trimmed.indexOf("#");
50
+ const loginPart = trimmed.substring(0, hashIdx);
51
+ const domainPart = trimmed.substring(hashIdx + 1);
52
+ if (
53
+ loginPart.length >= 1 &&
54
+ loginPart.length <= USERNAME_MAX_LENGTH &&
55
+ domainPart.length > 0 &&
56
+ /^[a-zA-Z0-9_]+$/.test(loginPart) &&
57
+ !loginPart.startsWith("_") &&
58
+ !loginPart.endsWith("_")
59
+ ) {
60
+ return "username";
61
+ }
62
+ return "unknown";
63
+ }
64
+
65
+ // Global username: no #, no @, no +
66
+ const re = new RegExp(
67
+ `^[a-zA-Z0-9_]{${minUsernameLength},${USERNAME_MAX_LENGTH}}$`,
68
+ );
69
+ if (re.test(trimmed) && !trimmed.startsWith("_") && !trimmed.endsWith("_")) {
70
+ return "username";
71
+ }
72
+
73
+ return "unknown";
74
+ }
75
+
76
+ /**
77
+ * Normalize principal value for server submission.
78
+ *
79
+ * - Email: lowercase, strip dots before @, strip +aliases
80
+ * - Phone: E.164 format via libphonenumber-js
81
+ * - Username: lowercase, validate per arch rules
82
+ *
83
+ * @param minUsernameLength - minimum chars for username (3 for login, 6 for register)
84
+ * Throws if value doesn't pass validation for detected type.
85
+ */
86
+ export function normalizePrincipal(
87
+ input: string,
88
+ minUsernameLength = 3,
89
+ ): NormalizedPrincipal {
90
+ const trimmed = input.trim();
91
+ const type = detectLoginInputType(trimmed, minUsernameLength);
92
+
93
+ switch (type) {
94
+ case "email": {
95
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
96
+ if (!emailRegex.test(trimmed)) {
97
+ throw new Error("Invalid email address");
98
+ }
99
+ // Remove dots before @, strip +alias, lowercase
100
+ const [localRaw, domain] = trimmed.split("@");
101
+ const local = localRaw.replace(/\./g, "").replace(/\+.*$/, "");
102
+ const normalized = `${local}@${domain}`.toLowerCase();
103
+ return { value: normalized, display: normalized, type: "email" };
104
+ }
105
+
106
+ case "phone": {
107
+ if (!isValidPhoneNumber(trimmed)) {
108
+ throw new Error("Invalid phone number");
109
+ }
110
+ const parsed = parsePhoneNumberFromString(trimmed);
111
+ if (!parsed) throw new Error("Cannot parse phone number");
112
+ const e164 = parsed.format("E.164");
113
+ return { value: e164, display: e164, type: "phone" };
114
+ }
115
+
116
+ case "username": {
117
+ // Federated username: login#domain
118
+ if (trimmed.includes("#")) {
119
+ const hashIdx = trimmed.indexOf("#");
120
+ const loginPart = trimmed.substring(0, hashIdx).toLowerCase();
121
+ const domainPart = trimmed.substring(hashIdx + 1).toLowerCase();
122
+
123
+ // No minUsernameLength for federated — org admin decides
124
+ const loginRe = new RegExp(`^[a-z0-9_]{1,${USERNAME_MAX_LENGTH}}$`);
125
+ if (
126
+ !loginRe.test(loginPart) ||
127
+ loginPart.startsWith("_") ||
128
+ loginPart.endsWith("_") ||
129
+ loginPart.includes("__")
130
+ ) {
131
+ throw new Error("Invalid username");
132
+ }
133
+ if (!domainPart || !/^[a-z0-9._-]+$/.test(domainPart)) {
134
+ throw new Error("Invalid federated domain");
135
+ }
136
+ const normalized = `${loginPart}#${domainPart}`;
137
+ return { value: normalized, display: trimmed, type: "username" };
138
+ }
139
+
140
+ // Global username
141
+ const normalized = trimmed.toLowerCase();
142
+ const re = new RegExp(
143
+ `^[a-z0-9_]{${minUsernameLength},${USERNAME_MAX_LENGTH}}$`,
144
+ );
145
+ if (
146
+ !re.test(normalized) ||
147
+ normalized.startsWith("_") ||
148
+ normalized.endsWith("_") ||
149
+ normalized.includes("__")
150
+ ) {
151
+ throw new Error("Invalid username");
152
+ }
153
+ return { value: normalized, display: trimmed, type: "username" };
154
+ }
155
+
156
+ default:
157
+ throw new Error("Cannot determine principal type");
158
+ }
159
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ isValidEmail,
4
+ isValidPhone,
5
+ isValidUuid,
6
+ isValidUsername,
7
+ minLength,
8
+ maxLength,
9
+ isRequired,
10
+ } from "./validators";
11
+
12
+ describe("isValidEmail", () => {
13
+ it("accepts valid emails", () => {
14
+ expect(isValidEmail("user@example.com")).toBe(true);
15
+ expect(isValidEmail("a.b+c@d.co")).toBe(true);
16
+ });
17
+ it("rejects invalid emails", () => {
18
+ expect(isValidEmail("")).toBe(false);
19
+ expect(isValidEmail("noat")).toBe(false);
20
+ expect(isValidEmail("@no-local.com")).toBe(false);
21
+ expect(isValidEmail("spaces in@email.com")).toBe(false);
22
+ });
23
+ });
24
+
25
+ describe("isValidPhone", () => {
26
+ it("accepts E.164 format", () => {
27
+ expect(isValidPhone("+380501234567")).toBe(true);
28
+ expect(isValidPhone("+14155552671")).toBe(true);
29
+ });
30
+ it("rejects non-E.164", () => {
31
+ expect(isValidPhone("0501234567")).toBe(false);
32
+ expect(isValidPhone("+0123")).toBe(false);
33
+ expect(isValidPhone("")).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe("isValidUuid", () => {
38
+ it("accepts valid UUIDs", () => {
39
+ expect(isValidUuid("550e8400-e29b-41d4-a716-446655440000")).toBe(true);
40
+ expect(isValidUuid("019577A0-2D3E-7B8E-9F1C-4A5B6C7D8E9F")).toBe(true);
41
+ });
42
+ it("rejects invalid UUIDs", () => {
43
+ expect(isValidUuid("not-a-uuid")).toBe(false);
44
+ expect(isValidUuid("")).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe("isValidUsername", () => {
49
+ it("accepts valid usernames", () => {
50
+ expect(isValidUsername("alice")).toBe(true);
51
+ expect(isValidUsername("bob-123")).toBe(true);
52
+ expect(isValidUsername("a_b")).toBe(true);
53
+ });
54
+ it("rejects invalid usernames", () => {
55
+ expect(isValidUsername("ab")).toBe(false); // too short
56
+ expect(isValidUsername("has spaces")).toBe(false);
57
+ expect(isValidUsername("special!char")).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe("minLength / maxLength / isRequired", () => {
62
+ it("minLength works", () => {
63
+ expect(minLength("abc", 3)).toBe(true);
64
+ expect(minLength("ab", 3)).toBe(false);
65
+ });
66
+ it("maxLength works", () => {
67
+ expect(maxLength("abc", 3)).toBe(true);
68
+ expect(maxLength("abcd", 3)).toBe(false);
69
+ });
70
+ it("isRequired works", () => {
71
+ expect(isRequired("hello")).toBe(true);
72
+ expect(isRequired(" ")).toBe(false);
73
+ expect(isRequired("")).toBe(false);
74
+ });
75
+ });
@@ -0,0 +1,36 @@
1
+ /** Email validation (RFC 5322 simplified). */
2
+ export function isValidEmail(value: string): boolean {
3
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
4
+ }
5
+
6
+ /** Phone number validation (E.164 format). */
7
+ export function isValidPhone(value: string): boolean {
8
+ return /^\+[1-9]\d{6,14}$/.test(value);
9
+ }
10
+
11
+ /** UUID v4/v7 format validation. */
12
+ export function isValidUuid(value: string): boolean {
13
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
14
+ value,
15
+ );
16
+ }
17
+
18
+ /** Username validation: letters, numbers, hyphens, underscores, 3-64 chars. */
19
+ export function isValidUsername(value: string): boolean {
20
+ return /^[a-zA-Z0-9_-]{3,64}$/.test(value);
21
+ }
22
+
23
+ /** Check minimum length. */
24
+ export function minLength(value: string, min: number): boolean {
25
+ return value.length >= min;
26
+ }
27
+
28
+ /** Check maximum length. */
29
+ export function maxLength(value: string, max: number): boolean {
30
+ return value.length <= max;
31
+ }
32
+
33
+ /** Check value is not empty/whitespace-only. */
34
+ export function isRequired(value: string): boolean {
35
+ return value.trim().length > 0;
36
+ }