@opentdf/sdk 0.9.0-rc.82 → 0.10.0-beta.95

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 (207) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/src/access/access-fetch.js +1 -2
  3. package/dist/cjs/src/access/access-rpc.js +1 -3
  4. package/dist/cjs/src/access.js +1 -14
  5. package/dist/cjs/src/auth/auth.js +13 -10
  6. package/dist/cjs/src/auth/dpop.js +121 -0
  7. package/dist/cjs/src/auth/oidc-clientcredentials-provider.js +37 -3
  8. package/dist/cjs/src/auth/oidc-externaljwt-provider.js +37 -3
  9. package/dist/cjs/src/auth/oidc-refreshtoken-provider.js +37 -3
  10. package/dist/cjs/src/auth/oidc.js +10 -8
  11. package/dist/cjs/src/auth/providers.js +35 -12
  12. package/dist/cjs/src/crypto/enums.js +1 -1
  13. package/dist/cjs/src/crypto/index.js +16 -2
  14. package/dist/cjs/src/crypto/pemPublicToCrypto.js +24 -20
  15. package/dist/cjs/src/errors.js +14 -2
  16. package/dist/cjs/src/index.js +8 -2
  17. package/dist/cjs/src/opentdf.js +50 -13
  18. package/dist/cjs/src/policy/discovery.js +188 -0
  19. package/dist/cjs/src/version.js +2 -2
  20. package/dist/cjs/tdf3/index.js +4 -2
  21. package/dist/cjs/tdf3/src/assertions.js +71 -31
  22. package/dist/cjs/tdf3/src/ciphers/aes-gcm-cipher.js +1 -1
  23. package/dist/cjs/tdf3/src/ciphers/symmetric-cipher-base.js +4 -2
  24. package/dist/cjs/tdf3/src/client/index.js +23 -33
  25. package/dist/cjs/tdf3/src/crypto/crypto-utils.js +12 -5
  26. package/dist/cjs/tdf3/src/crypto/declarations.js +1 -1
  27. package/dist/cjs/tdf3/src/crypto/index.js +849 -88
  28. package/dist/cjs/tdf3/src/crypto/jose/jwt-claims-set.js +11 -0
  29. package/dist/cjs/tdf3/src/crypto/jose/validate-crit.js +8 -0
  30. package/dist/cjs/tdf3/src/crypto/jose/vendor/lib/buffer_utils.js +41 -0
  31. package/dist/cjs/tdf3/src/crypto/jose/vendor/lib/epoch.js +6 -0
  32. package/dist/cjs/tdf3/src/crypto/jose/vendor/lib/is_object.js +21 -0
  33. package/dist/cjs/tdf3/src/crypto/jose/vendor/lib/jwt_claims_set.js +112 -0
  34. package/dist/cjs/tdf3/src/crypto/jose/vendor/lib/secs.js +60 -0
  35. package/dist/cjs/tdf3/src/crypto/jose/vendor/lib/validate_crit.js +38 -0
  36. package/dist/cjs/tdf3/src/crypto/jose/vendor/util/errors.js +135 -0
  37. package/dist/cjs/tdf3/src/crypto/jwt.js +183 -0
  38. package/dist/cjs/tdf3/src/crypto/salt.js +14 -8
  39. package/dist/cjs/tdf3/src/models/encryption-information.js +17 -20
  40. package/dist/cjs/tdf3/src/models/key-access.js +43 -63
  41. package/dist/cjs/tdf3/src/tdf.js +75 -75
  42. package/dist/cjs/tdf3/src/utils/index.js +5 -39
  43. package/dist/types/src/access/access-fetch.d.ts.map +1 -1
  44. package/dist/types/src/access/access-rpc.d.ts.map +1 -1
  45. package/dist/types/src/access.d.ts +0 -5
  46. package/dist/types/src/access.d.ts.map +1 -1
  47. package/dist/types/src/auth/auth.d.ts +9 -6
  48. package/dist/types/src/auth/auth.d.ts.map +1 -1
  49. package/dist/types/src/auth/dpop.d.ts +60 -0
  50. package/dist/types/src/auth/dpop.d.ts.map +1 -0
  51. package/dist/types/src/auth/oidc-clientcredentials-provider.d.ts +3 -2
  52. package/dist/types/src/auth/oidc-clientcredentials-provider.d.ts.map +1 -1
  53. package/dist/types/src/auth/oidc-externaljwt-provider.d.ts +3 -2
  54. package/dist/types/src/auth/oidc-externaljwt-provider.d.ts.map +1 -1
  55. package/dist/types/src/auth/oidc-refreshtoken-provider.d.ts +3 -2
  56. package/dist/types/src/auth/oidc-refreshtoken-provider.d.ts.map +1 -1
  57. package/dist/types/src/auth/oidc.d.ts +6 -4
  58. package/dist/types/src/auth/oidc.d.ts.map +1 -1
  59. package/dist/types/src/auth/providers.d.ts +5 -4
  60. package/dist/types/src/auth/providers.d.ts.map +1 -1
  61. package/dist/types/src/crypto/enums.d.ts +1 -1
  62. package/dist/types/src/crypto/index.d.ts +2 -1
  63. package/dist/types/src/crypto/index.d.ts.map +1 -1
  64. package/dist/types/src/crypto/pemPublicToCrypto.d.ts +18 -0
  65. package/dist/types/src/crypto/pemPublicToCrypto.d.ts.map +1 -1
  66. package/dist/types/src/errors.d.ts +8 -0
  67. package/dist/types/src/errors.d.ts.map +1 -1
  68. package/dist/types/src/index.d.ts +2 -1
  69. package/dist/types/src/index.d.ts.map +1 -1
  70. package/dist/types/src/opentdf.d.ts +26 -7
  71. package/dist/types/src/opentdf.d.ts.map +1 -1
  72. package/dist/types/src/policy/discovery.d.ts +74 -0
  73. package/dist/types/src/policy/discovery.d.ts.map +1 -0
  74. package/dist/types/src/version.d.ts +1 -1
  75. package/dist/types/src/version.d.ts.map +1 -1
  76. package/dist/types/tdf3/index.d.ts +3 -3
  77. package/dist/types/tdf3/index.d.ts.map +1 -1
  78. package/dist/types/tdf3/src/assertions.d.ts +23 -8
  79. package/dist/types/tdf3/src/assertions.d.ts.map +1 -1
  80. package/dist/types/tdf3/src/ciphers/aes-gcm-cipher.d.ts +3 -3
  81. package/dist/types/tdf3/src/ciphers/aes-gcm-cipher.d.ts.map +1 -1
  82. package/dist/types/tdf3/src/ciphers/symmetric-cipher-base.d.ts +4 -4
  83. package/dist/types/tdf3/src/ciphers/symmetric-cipher-base.d.ts.map +1 -1
  84. package/dist/types/tdf3/src/client/builders.d.ts +2 -2
  85. package/dist/types/tdf3/src/client/builders.d.ts.map +1 -1
  86. package/dist/types/tdf3/src/client/index.d.ts +6 -5
  87. package/dist/types/tdf3/src/client/index.d.ts.map +1 -1
  88. package/dist/types/tdf3/src/crypto/crypto-utils.d.ts +14 -4
  89. package/dist/types/tdf3/src/crypto/crypto-utils.d.ts.map +1 -1
  90. package/dist/types/tdf3/src/crypto/declarations.d.ts +283 -18
  91. package/dist/types/tdf3/src/crypto/declarations.d.ts.map +1 -1
  92. package/dist/types/tdf3/src/crypto/index.d.ts +105 -28
  93. package/dist/types/tdf3/src/crypto/index.d.ts.map +1 -1
  94. package/dist/types/tdf3/src/crypto/jose/jwt-claims-set.d.ts +3 -0
  95. package/dist/types/tdf3/src/crypto/jose/jwt-claims-set.d.ts.map +1 -0
  96. package/dist/types/tdf3/src/crypto/jose/validate-crit.d.ts +5 -0
  97. package/dist/types/tdf3/src/crypto/jose/validate-crit.d.ts.map +1 -0
  98. package/dist/types/tdf3/src/crypto/jose/vendor/lib/buffer_utils.d.ts +6 -0
  99. package/dist/types/tdf3/src/crypto/jose/vendor/lib/buffer_utils.d.ts.map +1 -0
  100. package/dist/types/tdf3/src/crypto/jose/vendor/lib/epoch.d.ts +3 -0
  101. package/dist/types/tdf3/src/crypto/jose/vendor/lib/epoch.d.ts.map +1 -0
  102. package/dist/types/tdf3/src/crypto/jose/vendor/lib/is_object.d.ts +3 -0
  103. package/dist/types/tdf3/src/crypto/jose/vendor/lib/is_object.d.ts.map +1 -0
  104. package/dist/types/tdf3/src/crypto/jose/vendor/lib/jwt_claims_set.d.ts +3 -0
  105. package/dist/types/tdf3/src/crypto/jose/vendor/lib/jwt_claims_set.d.ts.map +1 -0
  106. package/dist/types/tdf3/src/crypto/jose/vendor/lib/secs.d.ts +3 -0
  107. package/dist/types/tdf3/src/crypto/jose/vendor/lib/secs.d.ts.map +1 -0
  108. package/dist/types/tdf3/src/crypto/jose/vendor/lib/validate_crit.d.ts +3 -0
  109. package/dist/types/tdf3/src/crypto/jose/vendor/lib/validate_crit.d.ts.map +1 -0
  110. package/dist/types/tdf3/src/crypto/jose/vendor/util/errors.d.ts +76 -0
  111. package/dist/types/tdf3/src/crypto/jose/vendor/util/errors.d.ts.map +1 -0
  112. package/dist/types/tdf3/src/crypto/jwt.d.ts +76 -0
  113. package/dist/types/tdf3/src/crypto/jwt.d.ts.map +1 -0
  114. package/dist/types/tdf3/src/crypto/salt.d.ts +6 -1
  115. package/dist/types/tdf3/src/crypto/salt.d.ts.map +1 -1
  116. package/dist/types/tdf3/src/models/encryption-information.d.ts +4 -4
  117. package/dist/types/tdf3/src/models/encryption-information.d.ts.map +1 -1
  118. package/dist/types/tdf3/src/models/key-access.d.ts +8 -5
  119. package/dist/types/tdf3/src/models/key-access.d.ts.map +1 -1
  120. package/dist/types/tdf3/src/tdf.d.ts +8 -8
  121. package/dist/types/tdf3/src/tdf.d.ts.map +1 -1
  122. package/dist/types/tdf3/src/utils/index.d.ts +4 -3
  123. package/dist/types/tdf3/src/utils/index.d.ts.map +1 -1
  124. package/dist/web/src/access/access-fetch.js +3 -4
  125. package/dist/web/src/access/access-rpc.js +3 -5
  126. package/dist/web/src/access.js +1 -13
  127. package/dist/web/src/auth/auth.js +13 -10
  128. package/dist/web/src/auth/dpop.js +118 -0
  129. package/dist/web/src/auth/oidc-clientcredentials-provider.js +4 -3
  130. package/dist/web/src/auth/oidc-externaljwt-provider.js +4 -3
  131. package/dist/web/src/auth/oidc-refreshtoken-provider.js +4 -3
  132. package/dist/web/src/auth/oidc.js +11 -9
  133. package/dist/web/src/auth/providers.js +13 -12
  134. package/dist/web/src/crypto/enums.js +1 -1
  135. package/dist/web/src/crypto/index.js +4 -2
  136. package/dist/web/src/crypto/pemPublicToCrypto.js +18 -18
  137. package/dist/web/src/errors.js +12 -1
  138. package/dist/web/src/index.js +3 -2
  139. package/dist/web/src/opentdf.js +17 -13
  140. package/dist/web/src/policy/discovery.js +182 -0
  141. package/dist/web/src/version.js +2 -2
  142. package/dist/web/tdf3/index.js +3 -2
  143. package/dist/web/tdf3/src/assertions.js +71 -31
  144. package/dist/web/tdf3/src/ciphers/aes-gcm-cipher.js +1 -1
  145. package/dist/web/tdf3/src/ciphers/symmetric-cipher-base.js +4 -2
  146. package/dist/web/tdf3/src/client/index.js +25 -35
  147. package/dist/web/tdf3/src/crypto/crypto-utils.js +12 -5
  148. package/dist/web/tdf3/src/crypto/declarations.js +1 -1
  149. package/dist/web/tdf3/src/crypto/index.js +830 -84
  150. package/dist/web/tdf3/src/crypto/jose/jwt-claims-set.js +5 -0
  151. package/dist/web/tdf3/src/crypto/jose/validate-crit.js +3 -0
  152. package/dist/web/tdf3/src/crypto/jose/vendor/lib/buffer_utils.js +35 -0
  153. package/dist/web/tdf3/src/crypto/jose/vendor/lib/epoch.js +4 -0
  154. package/dist/web/tdf3/src/crypto/jose/vendor/lib/is_object.js +19 -0
  155. package/dist/web/tdf3/src/crypto/jose/vendor/lib/jwt_claims_set.js +107 -0
  156. package/dist/web/tdf3/src/crypto/jose/vendor/lib/secs.js +58 -0
  157. package/dist/web/tdf3/src/crypto/jose/vendor/lib/validate_crit.js +36 -0
  158. package/dist/web/tdf3/src/crypto/jose/vendor/util/errors.js +117 -0
  159. package/dist/web/tdf3/src/crypto/jwt.js +174 -0
  160. package/dist/web/tdf3/src/crypto/salt.js +13 -7
  161. package/dist/web/tdf3/src/models/encryption-information.js +11 -14
  162. package/dist/web/tdf3/src/models/key-access.js +44 -31
  163. package/dist/web/tdf3/src/tdf.js +71 -71
  164. package/dist/web/tdf3/src/utils/index.js +5 -6
  165. package/package.json +11 -4
  166. package/src/access/access-fetch.ts +2 -8
  167. package/src/access/access-rpc.ts +0 -7
  168. package/src/access.ts +0 -17
  169. package/src/auth/auth.ts +21 -12
  170. package/src/auth/dpop.ts +222 -0
  171. package/src/auth/oidc-clientcredentials-provider.ts +23 -15
  172. package/src/auth/oidc-externaljwt-provider.ts +23 -15
  173. package/src/auth/oidc-refreshtoken-provider.ts +23 -15
  174. package/src/auth/oidc.ts +21 -10
  175. package/src/auth/providers.ts +46 -29
  176. package/src/crypto/enums.ts +1 -1
  177. package/src/crypto/index.ts +21 -1
  178. package/src/crypto/pemPublicToCrypto.ts +18 -20
  179. package/src/errors.ts +9 -0
  180. package/src/index.ts +7 -0
  181. package/src/opentdf.ts +36 -17
  182. package/src/policy/discovery.ts +222 -0
  183. package/src/version.ts +1 -1
  184. package/tdf3/index.ts +32 -5
  185. package/tdf3/src/assertions.ts +99 -30
  186. package/tdf3/src/ciphers/aes-gcm-cipher.ts +7 -2
  187. package/tdf3/src/ciphers/symmetric-cipher-base.ts +7 -4
  188. package/tdf3/src/client/builders.ts +2 -2
  189. package/tdf3/src/client/index.ts +60 -59
  190. package/tdf3/src/crypto/crypto-utils.ts +15 -8
  191. package/tdf3/src/crypto/declarations.ts +338 -22
  192. package/tdf3/src/crypto/index.ts +1021 -118
  193. package/tdf3/src/crypto/jose/jwt-claims-set.ts +10 -0
  194. package/tdf3/src/crypto/jose/validate-crit.ts +9 -0
  195. package/tdf3/src/crypto/jose/vendor/lib/buffer_utils.ts +34 -0
  196. package/tdf3/src/crypto/jose/vendor/lib/epoch.ts +3 -0
  197. package/tdf3/src/crypto/jose/vendor/lib/is_object.ts +18 -0
  198. package/tdf3/src/crypto/jose/vendor/lib/jwt_claims_set.ts +106 -0
  199. package/tdf3/src/crypto/jose/vendor/lib/secs.ts +57 -0
  200. package/tdf3/src/crypto/jose/vendor/lib/validate_crit.ts +35 -0
  201. package/tdf3/src/crypto/jose/vendor/util/errors.ts +101 -0
  202. package/tdf3/src/crypto/jwt.ts +256 -0
  203. package/tdf3/src/crypto/salt.ts +16 -8
  204. package/tdf3/src/models/encryption-information.ts +14 -21
  205. package/tdf3/src/models/key-access.ts +57 -41
  206. package/tdf3/src/tdf.ts +110 -93
  207. package/tdf3/src/utils/index.ts +5 -6
@@ -0,0 +1,222 @@
1
+ import { ConnectError, Code } from '@connectrpc/connect';
2
+ import { AttributeNotFoundError, ConfigurationError, NetworkError } from '../errors.js';
3
+ import { type AuthProvider } from '../auth/auth.js';
4
+ import { extractRpcErrorMessage, validateSecureUrl } from '../utils.js';
5
+ import { PlatformClient } from '../platform.js';
6
+ import type { Attribute } from '../platform/policy/objects_pb.js';
7
+
8
+ // Caps the pagination loop in listAttributes. 10 pages × 1000 records = 10,000
9
+ // attributes maximum, which is generous for browser use while preventing runaway
10
+ // memory growth if a server repeatedly returns a non-zero next_offset.
11
+ const MAX_LIST_ATTRIBUTES_PAGES = 10;
12
+
13
+ // Number of attributes to request per page. Matches the platform's default
14
+ // (ListRequestLimitDefault = 1000) so behavior is stable regardless of server config.
15
+ const LIST_ATTRIBUTES_PAGE_SIZE = 1000;
16
+
17
+ // Matches the server-side proto constraint: GetAttributeValuesByFqnsRequest has
18
+ // max_items: 250 on the fqns field, so the client rejects oversized requests
19
+ // locally instead of receiving a cryptic server validation error.
20
+ const MAX_VALIDATE_FQNS = 250;
21
+
22
+ // Attribute value FQN format: https://<namespace>/attr/<name>/value/<value>
23
+ // Restricts to safe URL characters to prevent XSS via FQNs in error messages
24
+ const ATTRIBUTE_VALUE_FQN_RE =
25
+ /^https?:\/\/[a-zA-Z0-9._~%-]+\/attr\/[a-zA-Z0-9._~%-]+\/value\/[a-zA-Z0-9._~%-]+$/i;
26
+
27
+ // Attribute-level FQN format: https://<namespace>/attr/<name> (no /value/ segment)
28
+ // Restricts to safe URL characters to prevent XSS via FQNs in error messages
29
+ const ATTRIBUTE_FQN_RE = /^https?:\/\/[a-zA-Z0-9._~%-]+\/attr\/[a-zA-Z0-9._~%-]+$/i;
30
+
31
+ /**
32
+ * Returns all active attributes available on the platform, auto-paginating through all results.
33
+ * An optional namespace name or ID may be provided to filter results.
34
+ *
35
+ * Use this before calling `createTDF()` to see what attributes are available for data tagging.
36
+ *
37
+ * @param platformUrl The platform base URL.
38
+ * @param authProvider An auth provider for the request.
39
+ * @param namespace Optional namespace name or ID to filter results.
40
+ * @returns All active {@link Attribute} objects on the platform.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const attrs = await listAttributes(platformUrl, authProvider);
45
+ * for (const a of attrs) {
46
+ * console.log(a.fqn);
47
+ * }
48
+ * ```
49
+ */
50
+ export async function listAttributes(
51
+ platformUrl: string,
52
+ authProvider: AuthProvider,
53
+ namespace?: string
54
+ ): Promise<Attribute[]> {
55
+ if (!validateSecureUrl(platformUrl)) {
56
+ throw new ConfigurationError('platformUrl must use HTTPS protocol');
57
+ }
58
+ const platform = new PlatformClient({ authProvider, platformUrl });
59
+ const result: Attribute[] = [];
60
+ let nextOffset = 0;
61
+
62
+ for (let pages = 0; pages < MAX_LIST_ATTRIBUTES_PAGES; pages++) {
63
+ let resp;
64
+ try {
65
+ resp = await platform.v1.attributes.listAttributes({
66
+ namespace: namespace ?? '',
67
+ pagination: { offset: nextOffset, limit: LIST_ATTRIBUTES_PAGE_SIZE },
68
+ });
69
+ } catch (e) {
70
+ throw new NetworkError(`[ListAttributes] ${extractRpcErrorMessage(e)}`);
71
+ }
72
+
73
+ result.push(...resp.attributes);
74
+ nextOffset = resp.pagination?.nextOffset ?? 0;
75
+ if (nextOffset === 0) {
76
+ return result;
77
+ }
78
+ }
79
+
80
+ throw new ConfigurationError(
81
+ `listAttributes returned more than ${MAX_LIST_ATTRIBUTES_PAGES * LIST_ATTRIBUTES_PAGE_SIZE} attributes. Use the namespace parameter to narrow results.`
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Checks that all provided attribute value FQNs exist on the platform.
87
+ * Validates FQN format first, then verifies existence via the platform API.
88
+ *
89
+ * Use this before `createTDF()` to catch missing or misspelled attributes early
90
+ * instead of discovering the problem at decryption time.
91
+ *
92
+ * @param platformUrl The platform base URL.
93
+ * @param authProvider An auth provider for the request.
94
+ * @param fqns Attribute value FQNs to validate, in the form
95
+ * `https://<namespace>/attr/<name>/value/<value>`.
96
+ * @throws {@link AttributeNotFoundError} if any FQNs are not found on the platform.
97
+ * @throws {@link ConfigurationError} if the FQN format is invalid or there are too many FQNs.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * await validateAttributes(platformUrl, authProvider, [
102
+ * 'https://opentdf.io/attr/department/value/marketing',
103
+ * ]);
104
+ * // Safe to encrypt — all attributes confirmed present
105
+ * ```
106
+ */
107
+ export async function validateAttributes(
108
+ platformUrl: string,
109
+ authProvider: AuthProvider,
110
+ fqns: string[]
111
+ ): Promise<void> {
112
+ if (!fqns || fqns.length === 0) {
113
+ return;
114
+ }
115
+
116
+ if (!validateSecureUrl(platformUrl)) {
117
+ throw new ConfigurationError('platformUrl must use HTTPS protocol');
118
+ }
119
+
120
+ if (fqns.length > MAX_VALIDATE_FQNS) {
121
+ throw new ConfigurationError(
122
+ `too many attribute FQNs: ${fqns.length} exceeds maximum of ${MAX_VALIDATE_FQNS}`
123
+ );
124
+ }
125
+
126
+ for (const fqn of fqns) {
127
+ if (!ATTRIBUTE_VALUE_FQN_RE.test(fqn)) {
128
+ throw new ConfigurationError('invalid attribute value FQN format');
129
+ }
130
+ }
131
+
132
+ const platform = new PlatformClient({ authProvider, platformUrl });
133
+ let resp;
134
+ try {
135
+ resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns });
136
+ } catch (e) {
137
+ throw new NetworkError(`[GetAttributeValuesByFqns] ${extractRpcErrorMessage(e)}`);
138
+ }
139
+
140
+ const found = resp.fqnAttributeValues;
141
+ const missing = fqns.filter((fqn) => !(fqn in found));
142
+ if (missing.length > 0) {
143
+ throw new AttributeNotFoundError(`attribute not found: ${missing.length} FQN(s) missing`);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Reports whether the attribute definition identified by `attributeFqn` exists on the platform.
149
+ *
150
+ * `attributeFqn` should be an attribute-level FQN (no `/value/` segment):
151
+ * `https://<namespace>/attr/<attribute_name>`
152
+ *
153
+ * @param platformUrl The platform base URL.
154
+ * @param authProvider An auth provider for the request.
155
+ * @param attributeFqn The attribute-level FQN to check.
156
+ * @returns `true` if the attribute exists, `false` if it does not.
157
+ * @throws {@link ConfigurationError} if the FQN format is invalid or the URL is insecure.
158
+ * @throws {@link NetworkError} if a non-not-found service error occurs.
159
+ */
160
+ export async function attributeExists(
161
+ platformUrl: string,
162
+ authProvider: AuthProvider,
163
+ attributeFqn: string
164
+ ): Promise<boolean> {
165
+ if (!validateSecureUrl(platformUrl)) {
166
+ throw new ConfigurationError('platformUrl must use HTTPS protocol');
167
+ }
168
+
169
+ if (!ATTRIBUTE_FQN_RE.test(attributeFqn)) {
170
+ throw new ConfigurationError('invalid attribute FQN format');
171
+ }
172
+
173
+ const platform = new PlatformClient({ authProvider, platformUrl });
174
+ try {
175
+ await platform.v1.attributes.getAttribute({
176
+ identifier: { case: 'fqn', value: attributeFqn },
177
+ });
178
+ return true;
179
+ } catch (e) {
180
+ if (e instanceof ConnectError && e.code === Code.NotFound) {
181
+ return false;
182
+ }
183
+ throw new NetworkError(`[GetAttribute] ${extractRpcErrorMessage(e)}`);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Reports whether the attribute value FQN exists on the platform.
189
+ *
190
+ * `valueFqn` should be a full attribute value FQN (with `/value/` segment):
191
+ * `https://<namespace>/attr/<attribute_name>/value/<value>`
192
+ *
193
+ * @param platformUrl The platform base URL.
194
+ * @param authProvider An auth provider for the request.
195
+ * @param valueFqn The attribute value FQN to check.
196
+ * @returns `true` if the value exists, `false` if it does not.
197
+ * @throws {@link ConfigurationError} if the FQN format is invalid or the URL is insecure.
198
+ * @throws {@link NetworkError} if a service error occurs.
199
+ */
200
+ export async function attributeValueExists(
201
+ platformUrl: string,
202
+ authProvider: AuthProvider,
203
+ valueFqn: string
204
+ ): Promise<boolean> {
205
+ if (!validateSecureUrl(platformUrl)) {
206
+ throw new ConfigurationError('platformUrl must use HTTPS protocol');
207
+ }
208
+
209
+ if (!ATTRIBUTE_VALUE_FQN_RE.test(valueFqn)) {
210
+ throw new ConfigurationError('invalid attribute value FQN format');
211
+ }
212
+
213
+ const platform = new PlatformClient({ authProvider, platformUrl });
214
+ let resp;
215
+ try {
216
+ resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns: [valueFqn] });
217
+ } catch (e) {
218
+ throw new NetworkError(`[GetAttributeValuesByFqns] ${extractRpcErrorMessage(e)}`);
219
+ }
220
+
221
+ return valueFqn in resp.fqnAttributeValues;
222
+ }
package/src/version.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Exposes the released version number of the `@opentdf/sdk` package
3
3
  */
4
- export const version = '0.9.0'; // x-release-please-version
4
+ export const version = '0.10.0'; // x-release-please-version
5
5
 
6
6
  /**
7
7
  * A string name used to label requests as coming from this library client.
package/tdf3/index.ts CHANGED
@@ -14,10 +14,23 @@ import {
14
14
  } from './src/client/builders.js';
15
15
  import { type ClientConfig, createSessionKeys } from './src/client/index.js';
16
16
  import {
17
+ type AsymmetricSigningAlgorithm,
17
18
  type CryptoService,
18
19
  type DecryptResult,
20
+ type ECCurve,
19
21
  type EncryptResult,
22
+ type HashAlgorithm,
23
+ type HkdfParams,
24
+ type KeyPair,
25
+ type KeyOptions,
26
+ type KeyAlgorithm,
20
27
  type PemKeyPair,
28
+ type PrivateKey,
29
+ type PublicKey,
30
+ type PublicKeyInfo,
31
+ type SigningAlgorithm,
32
+ type SymmetricKey,
33
+ type SymmetricSigningAlgorithm,
21
34
  } from './src/crypto/declarations.js';
22
35
  import { Client, Errors, TDF3Client } from './src/index.js';
23
36
  import {
@@ -35,18 +48,31 @@ import { type Chunker } from '../src/seekable.js';
35
48
  export type {
36
49
  AlgorithmName,
37
50
  AlgorithmUrn,
51
+ AsymmetricSigningAlgorithm,
38
52
  AuthProvider,
39
53
  Chunker,
40
54
  CryptoService,
55
+ DecryptKeyMiddleware,
41
56
  DecryptResult,
57
+ DecryptStreamMiddleware,
58
+ ECCurve,
59
+ EncryptKeyMiddleware,
42
60
  EncryptResult,
61
+ EncryptStreamMiddleware,
62
+ HashAlgorithm,
63
+ HkdfParams,
43
64
  HttpMethod,
65
+ KeyPair,
66
+ KeyOptions,
67
+ KeyAlgorithm,
44
68
  PemKeyPair,
45
- EncryptKeyMiddleware,
46
- EncryptStreamMiddleware,
47
- DecryptKeyMiddleware,
48
- DecryptStreamMiddleware,
69
+ PrivateKey,
70
+ PublicKey,
71
+ PublicKeyInfo,
72
+ SigningAlgorithm,
49
73
  SplitStep,
74
+ SymmetricKey,
75
+ SymmetricSigningAlgorithm,
50
76
  };
51
77
 
52
78
  export {
@@ -74,7 +100,8 @@ export {
74
100
  version,
75
101
  };
76
102
 
77
- export * as WebCryptoService from './src/crypto/index.js';
103
+ export { DefaultCryptoService as WebCryptoService } from './src/crypto/index.js';
104
+ // export the other methods from crypto/index.js that aren't part of CryptoService but are needed for JWT handling
78
105
  export {
79
106
  type CreateOptions,
80
107
  type CreateZTDFOptions,
@@ -1,8 +1,14 @@
1
1
  import { canonicalizeEx } from 'json-canonicalize';
2
- import { SignJWT, jwtVerify, importJWK, importX509 } from 'jose';
3
2
  import { base64, hex } from '../../src/encodings/index.js';
4
3
  import { ConfigurationError, IntegrityError, InvalidFileError } from '../../src/errors.js';
5
4
  import { tdfSpecVersion, version as sdkVersion } from '../../src/version.js';
5
+ import {
6
+ type CryptoService,
7
+ type PrivateKey,
8
+ type PublicKey,
9
+ type SymmetricKey,
10
+ } from './crypto/declarations.js';
11
+ import { decodeProtectedHeader, signJwt, verifyJwt, type JwtHeader } from './crypto/jwt.js';
6
12
 
7
13
  export type AssertionKeyAlg = 'ES256' | 'RS256' | 'HS256';
8
14
  export type AssertionType = 'handling' | 'other';
@@ -41,39 +47,69 @@ export type AssertionPayload = {
41
47
  /**
42
48
  * Computes the SHA-256 hash of the assertion object, excluding the 'binding' and 'hash' properties.
43
49
  *
50
+ * @param a - The assertion to hash
51
+ * @param cryptoService - The crypto service to use for hashing
44
52
  * @returns the hexadecimal string representation of the hash
45
53
  */
46
- export async function hash(a: Assertion): Promise<string> {
54
+ export async function hash(a: Assertion, cryptoService: CryptoService): Promise<string> {
47
55
  const result = canonicalizeEx(a, {
48
56
  exclude: ['binding', 'hash', 'sign', 'verify', 'signingKey'],
49
57
  });
50
58
 
51
- const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(result));
52
- return hex.encodeArrayBuffer(hash);
59
+ const hashBytes = await cryptoService.digest('SHA-256', new TextEncoder().encode(result));
60
+ return hex.encodeArrayBuffer(hashBytes.buffer);
53
61
  }
54
62
 
55
63
  /**
56
64
  * Signs the given hash and signature using the provided key and sets the binding method and signature.
57
65
  *
58
- * @param hash - The hash to be signed.
66
+ * @param thiz - The assertion to sign.
67
+ * @param assertionHash - The hash to be signed.
59
68
  * @param sig - The signature to be signed.
60
- * @param {AssertionKey} key - The key used for signing.
61
- * @returns {Promise<void>} A promise that resolves when the signing is complete.
69
+ * @param key - The key used for signing.
70
+ * @param cryptoService - The crypto service to use for signing.
71
+ * @returns A promise that resolves to the signed assertion.
62
72
  */
63
73
  async function sign(
64
74
  thiz: Assertion,
65
75
  assertionHash: string,
66
76
  sig: string,
67
- key: AssertionKey
77
+ key: AssertionKey,
78
+ cryptoService: CryptoService
68
79
  ): Promise<Assertion> {
69
80
  const payload: AssertionPayload = {
70
81
  assertionHash,
71
82
  assertionSig: sig,
72
83
  };
73
84
 
85
+ const header: JwtHeader = { alg: key.alg };
86
+
87
+ if (typeof key.key === 'object' && '_brand' in key.key && key.key._brand === 'PublicKey') {
88
+ throw new ConfigurationError(
89
+ 'Cannot sign assertion with PublicKey. Use PrivateKey or SymmetricKey for signing.'
90
+ );
91
+ }
92
+
93
+ let signingMaterial: PrivateKey | SymmetricKey;
94
+ if (typeof key.key === 'string') {
95
+ if (!cryptoService.importPrivateKey) {
96
+ throw new ConfigurationError(
97
+ 'CryptoService does not support importing private keys. Cannot sign assertion with a PEM string. Use PrivateKey or SymmetricKey for signing.'
98
+ );
99
+ }
100
+ signingMaterial = await cryptoService.importPrivateKey(key.key, {
101
+ usage: 'sign',
102
+ extractable: false,
103
+ });
104
+ } else if (key.key instanceof Uint8Array) {
105
+ signingMaterial = await cryptoService.importSymmetricKey(key.key);
106
+ } else {
107
+ signingMaterial = key.key as PrivateKey | SymmetricKey;
108
+ }
109
+
74
110
  let token: string;
75
111
  try {
76
- token = await new SignJWT(payload).setProtectedHeader({ alg: key.alg }).sign(key.key);
112
+ token = await signJwt(cryptoService, payload, signingMaterial, header);
77
113
  } catch (error) {
78
114
  throw new ConfigurationError(`Signing assertion failed: ${error.message}`, error);
79
115
  }
@@ -107,36 +143,54 @@ export function isAssertionConfig(obj: unknown): obj is AssertionConfig {
107
143
  /**
108
144
  * Verifies the signature of the assertion using the provided key.
109
145
  *
110
- * @param {AssertionKey} key - The key used for verification.
111
- * @returns {Promise<[string, string]>} A promise that resolves to a tuple containing the assertion hash and signature.
112
- * @throws {Error} If the verification fails.
146
+ * @param thiz - The assertion to verify.
147
+ * @param aggregateHash - The aggregate hash for integrity checking.
148
+ * @param key - The key used for verification.
149
+ * @param isLegacyTDF - Whether this is a legacy TDF format.
150
+ * @param cryptoService - The crypto service to use for verification.
151
+ * @throws {InvalidFileError} If the verification fails.
152
+ * @throws {IntegrityError} If the integrity check fails.
113
153
  */
114
154
  export async function verify(
115
155
  thiz: Assertion,
116
156
  aggregateHash: Uint8Array,
117
157
  key: AssertionKey,
118
- isLegacyTDF: boolean
158
+ isLegacyTDF: boolean,
159
+ cryptoService: CryptoService
119
160
  ): Promise<void> {
120
161
  let payload: AssertionPayload;
121
162
  try {
122
- const uj = await jwtVerify(thiz.binding.signature, async (header) => {
123
- if (header.jwk) {
124
- return await importJWK(header.jwk, header.alg);
125
- }
126
- if (header.x5c && header.x5c.length > 0) {
127
- const cert = `-----BEGIN CERTIFICATE-----\n${header.x5c[0]}\n-----END CERTIFICATE-----`;
128
- return await importX509(cert, header.alg);
129
- }
130
- return key.key;
163
+ // Parse JWT header to check for embedded keys (jwk or x5c)
164
+ const header = decodeProtectedHeader(thiz.binding.signature);
165
+
166
+ // Runtime check: ensure we have a verification key, not a signing key
167
+ if (typeof key.key === 'object' && '_brand' in key.key && key.key._brand === 'PrivateKey') {
168
+ throw new ConfigurationError(
169
+ 'Cannot verify assertion with PrivateKey. Use PublicKey or SymmetricKey for verification.'
170
+ );
171
+ }
172
+ let verificationKey: string | Uint8Array | PublicKey | SymmetricKey = key.key;
173
+
174
+ if (header.jwk) {
175
+ // Convert embedded JWK to PEM
176
+ verificationKey = await cryptoService.jwkToPublicKeyPem(header.jwk as JsonWebKey);
177
+ } else if (header.x5c && Array.isArray(header.x5c) && header.x5c.length > 0) {
178
+ // Extract public key from X.509 certificate
179
+ const cert = `-----BEGIN CERTIFICATE-----\n${header.x5c[0]}\n-----END CERTIFICATE-----`;
180
+ verificationKey = await cryptoService.extractPublicKeyPem(cert);
181
+ }
182
+
183
+ const result = await verifyJwt(cryptoService, thiz.binding.signature, verificationKey, {
184
+ algorithms: [key.alg],
131
185
  });
132
- payload = uj.payload as AssertionPayload;
186
+ payload = result.payload as AssertionPayload;
133
187
  } catch (error) {
134
188
  throw new InvalidFileError(`Verifying assertion failed: ${error.message}`, error);
135
189
  }
136
190
  const { assertionHash, assertionSig } = payload;
137
191
 
138
192
  // Get the hash of the assertion
139
- const hashOfAssertion = await hash(thiz);
193
+ const hashOfAssertion = await hash(thiz, cryptoService);
140
194
 
141
195
  // check if assertionHash is same as hashOfAssertion
142
196
  if (hashOfAssertion !== assertionHash) {
@@ -164,13 +218,17 @@ export async function verify(
164
218
 
165
219
  /**
166
220
  * Creates an Assertion object with the specified properties.
167
- */
168
- /**
169
- * Creates an Assertion object with the specified properties.
221
+ *
222
+ * @param aggregateHash - The aggregate hash for the assertion.
223
+ * @param assertionConfig - The configuration for the assertion.
224
+ * @param cryptoService - The crypto service to use for signing.
225
+ * @param targetVersion - The target TDF spec version.
226
+ * @returns The created assertion.
170
227
  */
171
228
  export async function CreateAssertion(
172
229
  aggregateHash: Uint8Array | string,
173
230
  assertionConfig: AssertionConfig,
231
+ cryptoService: CryptoService,
174
232
  targetVersion?: string
175
233
  ): Promise<Assertion> {
176
234
  if (!assertionConfig.signingKey) {
@@ -187,7 +245,7 @@ export async function CreateAssertion(
187
245
  binding: { method: '', signature: '' },
188
246
  };
189
247
 
190
- const assertionHash = await hash(a);
248
+ const assertionHash = await hash(a, cryptoService);
191
249
  let encodedHash: string;
192
250
  switch (targetVersion || '4.3.0') {
193
251
  case '4.2.2':
@@ -212,12 +270,23 @@ export async function CreateAssertion(
212
270
  throw new ConfigurationError(`Unsupported TDF spec version: [${targetVersion}]`);
213
271
  }
214
272
 
215
- return await sign(a, assertionHash, encodedHash, assertionConfig.signingKey);
273
+ return await sign(a, assertionHash, encodedHash, assertionConfig.signingKey, cryptoService);
216
274
  }
217
275
 
276
+ // TODO: Split AssertionKey into two separate types:
277
+ // - AssertionSigningKey: key restricted to PrivateKey | SymmetricKey (no strings, no raw bytes)
278
+ // - AssertionVerificationKey: key restricted to string | PublicKey | SymmetricKey
279
+ // This would make the signing/verification distinction type-safe rather than relying on runtime checks.
280
+ // AssertionConfig.signingKey would use AssertionSigningKey; verify() and AssertionVerificationKeys would use AssertionVerificationKey.
281
+
282
+ /**
283
+ * Key used for signing or verifying assertions.
284
+ * For asymmetric algorithms (RS256, ES256): PEM string, PrivateKey (for signing), or PublicKey (for verification).
285
+ * For symmetric algorithms (HS256): Uint8Array or SymmetricKey (opaque).
286
+ */
218
287
  export type AssertionKey = {
219
288
  alg: AssertionKeyAlg;
220
- key: CryptoKey | Uint8Array;
289
+ key: string | Uint8Array | PrivateKey | PublicKey | SymmetricKey;
221
290
  };
222
291
 
223
292
  // AssertionConfig is a shadow of Assertion with the addition of the signing key.
@@ -7,6 +7,7 @@ import {
7
7
  type CryptoService,
8
8
  type DecryptResult,
9
9
  type EncryptResult,
10
+ type SymmetricKey,
10
11
  } from '../crypto/declarations.js';
11
12
 
12
13
  const KEY_LENGTH = 32;
@@ -45,7 +46,7 @@ export class AesGcmCipher extends SymmetricCipher {
45
46
  * result from the crypto service and construct the payload automatically from
46
47
  * it's parts. There is no need to process the payload.
47
48
  */
48
- override async encrypt(payload: Binary, key: Binary, iv: Binary): Promise<EncryptResult> {
49
+ override async encrypt(payload: Binary, key: SymmetricKey, iv: Binary): Promise<EncryptResult> {
49
50
  const toConcat: Uint8Array[] = [];
50
51
  const result = await this.cryptoService.encrypt(payload, key, iv, Algorithms.AES_256_GCM);
51
52
  toConcat.push(new Uint8Array(iv.asArrayBuffer()));
@@ -62,7 +63,11 @@ export class AesGcmCipher extends SymmetricCipher {
62
63
  * @returns
63
64
  */
64
65
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
65
- override async decrypt(buffer: ArrayBuffer, key: Binary, iv?: Binary): Promise<DecryptResult> {
66
+ override async decrypt(
67
+ buffer: ArrayBuffer,
68
+ key: SymmetricKey,
69
+ iv?: Binary
70
+ ): Promise<DecryptResult> {
66
71
  const { payload, payloadIv, payloadAuthTag } = processGcmPayload(buffer);
67
72
 
68
73
  return this.cryptoService.decrypt(
@@ -3,7 +3,9 @@ import {
3
3
  type CryptoService,
4
4
  type DecryptResult,
5
5
  type EncryptResult,
6
+ type SymmetricKey,
6
7
  } from '../crypto/declarations.js';
8
+ import { encodeArrayBuffer as hexEncode } from '../../../src/encodings/hex.js';
7
9
 
8
10
  export abstract class SymmetricCipher {
9
11
  cryptoService: CryptoService;
@@ -22,17 +24,18 @@ export abstract class SymmetricCipher {
22
24
  if (!this.ivLength) {
23
25
  throw Error('No iv length');
24
26
  }
25
- return this.cryptoService.generateInitializationVector(this.ivLength);
27
+ const bytes = await this.cryptoService.randomBytes(this.ivLength);
28
+ return hexEncode(bytes.buffer);
26
29
  }
27
30
 
28
- async generateKey(): Promise<string> {
31
+ async generateKey(): Promise<SymmetricKey> {
29
32
  if (!this.keyLength) {
30
33
  throw Error('No key length');
31
34
  }
32
35
  return this.cryptoService.generateKey(this.keyLength);
33
36
  }
34
37
 
35
- abstract encrypt(payload: Binary, key: Binary, iv: Binary): Promise<EncryptResult>;
38
+ abstract encrypt(payload: Binary, key: SymmetricKey, iv: Binary): Promise<EncryptResult>;
36
39
 
37
- abstract decrypt(payload: Uint8Array, key: Binary, iv?: Binary): Promise<DecryptResult>;
40
+ abstract decrypt(payload: Uint8Array, key: SymmetricKey, iv?: Binary): Promise<DecryptResult>;
38
41
  }
@@ -4,7 +4,7 @@ import { type Metadata } from '../tdf.js';
4
4
  import { Binary } from '../binary.js';
5
5
 
6
6
  import { ConfigurationError } from '../../../src/errors.js';
7
- import { PemKeyPair } from '../crypto/declarations.js';
7
+ import { PemKeyPair, type SymmetricKey } from '../crypto/declarations.js';
8
8
  import { DecoratedReadableStream } from './DecoratedReadableStream.js';
9
9
  import { type Chunker } from '../../../src/seekable.js';
10
10
  import { AssertionConfig, AssertionVerificationKeys } from '../assertions.js';
@@ -516,7 +516,7 @@ class EncryptParamsBuilder {
516
516
  }
517
517
  }
518
518
 
519
- export type DecryptKeyMiddleware = (key: Binary) => Promise<Binary>;
519
+ export type DecryptKeyMiddleware = (key: SymmetricKey) => Promise<SymmetricKey>;
520
520
 
521
521
  export type DecryptStreamMiddleware = (
522
522
  stream: DecoratedReadableStream