@did-btcr2/common 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/canonicalization.js +7 -7
- package/dist/cjs/canonicalization.js.map +1 -1
- package/dist/cjs/constants.js +0 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/errors.js +20 -3
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils/date.js +92 -1
- package/dist/cjs/utils/date.js.map +1 -1
- package/dist/esm/canonicalization.js +7 -7
- package/dist/esm/canonicalization.js.map +1 -1
- package/dist/esm/constants.js +0 -1
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/errors.js +20 -3
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils/date.js +92 -1
- package/dist/esm/utils/date.js.map +1 -1
- package/dist/types/canonicalization.d.ts +5 -5
- package/dist/types/canonicalization.d.ts.map +1 -1
- package/dist/types/constants.d.ts +0 -1
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/errors.d.ts +14 -3
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/interfaces.d.ts +0 -271
- package/dist/types/interfaces.d.ts.map +1 -1
- package/dist/types/types.d.ts +1 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/date.d.ts +21 -1
- package/dist/types/utils/date.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/canonicalization.ts +15 -15
- package/src/constants.ts +0 -1
- package/src/errors.ts +25 -3
- package/src/interfaces.ts +0 -300
- package/src/types.ts +1 -0
- package/src/utils/date.ts +99 -1
package/src/interfaces.ts
CHANGED
|
@@ -9,303 +9,3 @@ export interface PatchOperation {
|
|
|
9
9
|
value?: unknown; // Required for add, replace, test
|
|
10
10
|
from?: string; // Required for move, copy
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The unsigned payload object containing instructions for how to update a
|
|
15
|
-
* did:btcr2 DID Document. Once signed, it becomes a
|
|
16
|
-
* {@link DidUpdateInvocation | DID Update Invocation}
|
|
17
|
-
*
|
|
18
|
-
* DID BTCR2
|
|
19
|
-
* {@link https://dcdpr.github.io/did-btcr2/#construct-did-update-payload | 4.3.1 Construct DID Update Payload}.
|
|
20
|
-
*
|
|
21
|
-
* Found in DID BTCR2 Specification {@link https://dcdpr.github.io/did-btcr2/#dereference-root-capability-identifier | Section 9.4.2}
|
|
22
|
-
* @example
|
|
23
|
-
* ```
|
|
24
|
-
* {
|
|
25
|
-
* "@context": [
|
|
26
|
-
* "https://w3id.org/zcap/v1",
|
|
27
|
-
* "https://w3id.org/security/data-integrity/v2",
|
|
28
|
-
* "https://w3id.org/json-ld-patch/v1"
|
|
29
|
-
* ],
|
|
30
|
-
* "patch": [
|
|
31
|
-
* {
|
|
32
|
-
* "op": "add",
|
|
33
|
-
* "path": "/service/4",
|
|
34
|
-
* "value": {
|
|
35
|
-
* "id": "#linked-domain",
|
|
36
|
-
* "type": "LinkedDomains",
|
|
37
|
-
* "serviceEndpoint": "https://contact-me.com"
|
|
38
|
-
* }
|
|
39
|
-
* }
|
|
40
|
-
* ],
|
|
41
|
-
* "proof":{
|
|
42
|
-
* "type": "DataIntegrityProof,
|
|
43
|
-
* "cryptosuite": "schnorr-secp256k1-jcs-2025,
|
|
44
|
-
* "verificationMethod": "did:btcr2:k1qqpuwwde82nennsavvf0lqfnlvx7frrgzs57lchr02q8mz49qzaaxmqphnvcx#initialKey,
|
|
45
|
-
* "invocationTarget": "did:btcr2:k1qqpuwwde82nennsavvf0lqfnlvx7frrgzs57lchr02q8mz49qzaaxmqphnvcx,
|
|
46
|
-
* "capability": "urn:zcap:root:did%3Abtcr2%3Ak1qqpuwwde82nennsavvf0lqfnlvx7frrgzs57lchr02q8mz49qzaaxmqphnvcx,
|
|
47
|
-
* "capabilityAction": "Write,
|
|
48
|
-
* "proofPurpose": "assertionMethod,
|
|
49
|
-
* "proofValue": "z381yXYmxU8NudZ4HXY56DfMN6zfD8syvWcRXzT9xD9uYoQToo8QsXD7ahM3gXTzuay5WJbqTswt2BKaGWYn2hHhVFKJLXaD
|
|
50
|
-
* }
|
|
51
|
-
* }
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
export interface DidUpdatePayload {
|
|
55
|
-
/**
|
|
56
|
-
* JSON-LD context URIs for interpreting this payload, including contexts
|
|
57
|
-
* for ZCAP (capabilities), Data Integrity proofs, and JSON-LD patch ops.
|
|
58
|
-
*/
|
|
59
|
-
'@context': string[];
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* A JSON Patch (or JSON-LD Patch) object defining the mutations to apply to
|
|
63
|
-
* the DID Document. Applying this patch to the current DID Document yields
|
|
64
|
-
* the new DID Document (which must remain valid per DID Core spec).
|
|
65
|
-
*/
|
|
66
|
-
patch: JsonPatch;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The multihash of the current (source) DID Document, encoded as a multibase
|
|
70
|
-
* base58-btc string. This is a SHA-256 hash of the canonicalized source DID
|
|
71
|
-
* Document, used to ensure the patch is applied to the correct document state.
|
|
72
|
-
*/
|
|
73
|
-
sourceHash: string;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The multihash of the updated (target) DID Document, encoded as multibase
|
|
77
|
-
* base58-btc. This is the SHA-256 hash of the canonicalized
|
|
78
|
-
* DID Document after applying the patch, used to verify the update result.
|
|
79
|
-
*/
|
|
80
|
-
targetHash: string;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The version number of the DID Document after this update.
|
|
84
|
-
* It is equal to the previous document version + 1.
|
|
85
|
-
*/
|
|
86
|
-
targetVersionId: number;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* A proof object (Data Integrity proof) that authorizes this update.
|
|
90
|
-
* It is a JSON-LD proof indicating a capability invocation on the DID's
|
|
91
|
-
* root capability, typically signed with the DID's verification key (using
|
|
92
|
-
* Schnorr secp256k1 in did:btcr2).
|
|
93
|
-
*/
|
|
94
|
-
proof?: Proof;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* An extension of {@link DidUpdatePayload | DID Update Payload} containing a
|
|
99
|
-
* Data Integrity proof that authorizes the update. Once signed, the spec calls
|
|
100
|
-
* this an 'invoked DID Update Payload' or 'didUpdateInvocation'.
|
|
101
|
-
*
|
|
102
|
-
* DID BTCR2
|
|
103
|
-
* {@link https://dcdpr.github.io/did-btcr2/#invoke-did-update-payload | 4.3.2 Invoke DID Update Payload}
|
|
104
|
-
* and
|
|
105
|
-
* {@link https://dcdpr.github.io/did-btcr2/#root-didbtcr2-update-capabilities | 9.4 Root did:btcr2 Update Capabilities}.
|
|
106
|
-
*/
|
|
107
|
-
export interface DidUpdateInvocation extends DidUpdatePayload {
|
|
108
|
-
proof: Proof;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Proof is the Data Integrity proof (ZCAP-LD style) added to a did:btcr2 DID
|
|
113
|
-
* Update Payload.
|
|
114
|
-
*
|
|
115
|
-
* Verifiable Credential Data Integrity
|
|
116
|
-
* {@link https://w3c.github.io/vc-data-integrity/#proofs | 2.1 Proofs}.
|
|
117
|
-
*
|
|
118
|
-
* DID BTCR2
|
|
119
|
-
* {@link https://dcdpr.github.io/did-btcr2/#invoke-did-update-payload | 4.3.2 Invoke DID Update Payload}.
|
|
120
|
-
*/
|
|
121
|
-
export interface Proof extends ProofOptions {
|
|
122
|
-
/**
|
|
123
|
-
* The cryptographic signature value. The exact property name may be defined
|
|
124
|
-
* by the cryptosuite (for instance, `proofValue` for a raw signature) and
|
|
125
|
-
* contains the actual signature bytes in an encoded form.
|
|
126
|
-
*/
|
|
127
|
-
proofValue: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Proof Options used when adding a Data Integrity proof (ZCAP-LD style)
|
|
132
|
-
* to a did:btcr2 DID Update Payload.
|
|
133
|
-
*
|
|
134
|
-
* Verifiable Credential Data Integrity
|
|
135
|
-
* {@link https://w3c.github.io/vc-data-integrity/#proofs | 2.1 Proofs}.
|
|
136
|
-
*
|
|
137
|
-
* DID BTCR2
|
|
138
|
-
* {@link https://dcdpr.github.io/did-btcr2/#invoke-did-update-payload | 4.3.2 Invoke DID Update Payload}.
|
|
139
|
-
*/
|
|
140
|
-
export interface ProofOptions {
|
|
141
|
-
/**
|
|
142
|
-
* The proof type—per the spec’s example, "DataIntegrityProof".
|
|
143
|
-
*/
|
|
144
|
-
type: string;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The cryptographic suite used, e.g. "schnorr-secp256k1-jcs-2025".
|
|
148
|
-
*/
|
|
149
|
-
cryptosuite: string;
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* DID URL of the key invoking the capability, i.e. the DID
|
|
153
|
-
* Document's verificationMethod.id used to sign this update.
|
|
154
|
-
*/
|
|
155
|
-
verificationMethod: string;
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* The purpose of the proof, which the spec sets to "capabilityInvocation".
|
|
159
|
-
*/
|
|
160
|
-
proofPurpose: string;
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The root capability being invoked. In did:btcr2, this is typically
|
|
164
|
-
* `urn:zcap:root:<urlencoded-did>` (see Section 9.4.1).
|
|
165
|
-
*/
|
|
166
|
-
capability?: string;
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* The action performed under the capability—set to "Write" in the spec
|
|
170
|
-
* for DID document updates.
|
|
171
|
-
*/
|
|
172
|
-
capabilityAction?: string;
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* (Optional) Some cryptosuites or proofs may include a timestamp, domain,
|
|
176
|
-
* or challenge. Although not explicitly required in the doc's steps, they
|
|
177
|
-
* often appear in Data Integrity proofs and may be included as needed.
|
|
178
|
-
*/
|
|
179
|
-
created?: string;
|
|
180
|
-
domain?: string;
|
|
181
|
-
challenge?: string;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* A JSON object that maps did:btcr2 identifiers to the CID of the corresponding
|
|
186
|
-
* DID Update Payload.
|
|
187
|
-
*
|
|
188
|
-
* DID BTCR2
|
|
189
|
-
* {@link https://dcdpr.github.io/did-btcr2/#cidaggregate-beacon | 5.2 CIDAggregate Beacons}.
|
|
190
|
-
*/
|
|
191
|
-
export interface DidUpdateBundle {
|
|
192
|
-
/**
|
|
193
|
-
* The keys are did:btcr2 identifiers as strings. The values are
|
|
194
|
-
* IPFS CIDs (or other CAS IDs) referencing the actual DID Update Payload.
|
|
195
|
-
*/
|
|
196
|
-
[didbtcr2Identifier: string]: string;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* A container for out-of-band data the resolver may need. This includes the
|
|
201
|
-
* initial DID document if it isn't stored in IPFS, plus references for each
|
|
202
|
-
* on-chain Beacon signal.
|
|
203
|
-
*
|
|
204
|
-
* DID BTCR2
|
|
205
|
-
* {@link https://dcdpr.github.io/did-btcr2/#sidecar-initial-document-validation | 4.2.1.2.1 Sidecar Initial Document Validation},
|
|
206
|
-
* {@link https://dcdpr.github.io/did-btcr2/#resolve-target-document | 4.2.2 Resolve Target Document},
|
|
207
|
-
* {@link https://dcdpr.github.io/did-btcr2/#traverse-blockchain-history | 4.2.2.2 Traverse Blockchain History},
|
|
208
|
-
* {@link https://dcdpr.github.io/did-btcr2/#find-next-signals | 4.2.2.3 Find Next Signals}.
|
|
209
|
-
*/
|
|
210
|
-
export interface SidecarData {
|
|
211
|
-
/**
|
|
212
|
-
* The initial DID Document for an externally created did:btcr2,
|
|
213
|
-
* if not fetched from IPFS or another CAS.
|
|
214
|
-
*/
|
|
215
|
-
initialDocument?: Record<string, any>; // or a typed DIDDocument from W3C DID Core
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* A map from Bitcoin transaction IDs to the sidecar info about that signal.
|
|
219
|
-
* Each signal might provide a single DID Update Payload, or (for aggregator beacons)
|
|
220
|
-
* a bundle or proofs.
|
|
221
|
-
*/
|
|
222
|
-
signalsMetadata: {
|
|
223
|
-
[txid: string]: SignalSidecarData;
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Sidecar data for a specific Beacon Signal. Different Beacon types store different fields.
|
|
229
|
-
* - SingletonBeacon might just store one `updatePayload`.
|
|
230
|
-
* - CIDAggregateBeacon might store `updateBundle` + an `updatePayload`.
|
|
231
|
-
* - SMTAggregateBeacon might store `updatePayload` + a `smtProof`.
|
|
232
|
-
*/
|
|
233
|
-
export interface SignalSidecarData {
|
|
234
|
-
updatePayload?: DidUpdateInvocation; // or DidUpdatePayload if not yet invoked
|
|
235
|
-
updateBundle?: DidUpdateBundle; // for CIDAggregateBeacon
|
|
236
|
-
/**
|
|
237
|
-
* For SMTAggregateBeacon, a Merkle proof that the `updatePayload`
|
|
238
|
-
* is included (or not included) in the aggregator's Sparse Merkle Tree.
|
|
239
|
-
*/
|
|
240
|
-
smtProof?: SmtProof;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* A placeholder for the actual Sparse Merkle Tree inclusion/non-inclusion proof.
|
|
245
|
-
*
|
|
246
|
-
* DID BTCR2
|
|
247
|
-
* {@link https://dcdpr.github.io/did-btcr2/#smtaggregate-beacon | 5.3 SMTAggregate Beacon}.
|
|
248
|
-
*/
|
|
249
|
-
export interface SmtProof {
|
|
250
|
-
// Implementation-specific structure for SMT proofs, e.g.:
|
|
251
|
-
siblingHashes: string[];
|
|
252
|
-
leafIndex?: string;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* The known Beacon types from the spec.
|
|
257
|
-
*/
|
|
258
|
-
export type BeaconType =
|
|
259
|
-
| 'SingletonBeacon'
|
|
260
|
-
| 'CIDAggregateBeacon'
|
|
261
|
-
| 'SMTAggregateBeacon';
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Represents a transaction discovered on the Bitcoin blockchain that
|
|
265
|
-
* spends from a Beacon address, thus announcing DID updates.
|
|
266
|
-
*
|
|
267
|
-
* DID BTCR2
|
|
268
|
-
* {@link https://dcdpr.github.io/did-btcr2/#find-next-signals | 4.2.2.3 Find Next Signals}
|
|
269
|
-
* and
|
|
270
|
-
* {@link https://dcdpr.github.io/did-btcr2/#process-beacon-signals | 4.2.2.4 Process Beacon Signals}.
|
|
271
|
-
*/
|
|
272
|
-
export interface BeaconSignal {
|
|
273
|
-
/**
|
|
274
|
-
* The DID Document's `service` ID of the Beacon that produced this signal, e.g. "#cidAggregateBeacon".
|
|
275
|
-
*/
|
|
276
|
-
beaconId: string;
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* The type of Beacon, e.g. "SingletonBeacon".
|
|
280
|
-
*/
|
|
281
|
-
beaconType: BeaconType;
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* The Bitcoin transaction that is the actual on-chain Beacon Signal.
|
|
285
|
-
* Typically you'd store a minimal subset or a reference/ID for real usage.
|
|
286
|
-
*/
|
|
287
|
-
tx: any;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* A ZCAP-LD root capability object that authorizes updates for a particular did:btcr2.
|
|
292
|
-
*
|
|
293
|
-
* DID BTCR2
|
|
294
|
-
* {@link https://dcdpr.github.io/did-btcr2/#derive-root-capability-from-didbtcr2-identifier | 9.4.1 Derive Root Capability from did:btcr2 Identifier}.
|
|
295
|
-
*
|
|
296
|
-
* @example Found in DID BTCR2 Specification Section 9.4.1
|
|
297
|
-
* ```
|
|
298
|
-
* {
|
|
299
|
-
* "@context": "https://w3id.org/zcap/v1",
|
|
300
|
-
* "id": "urn:zcap:root:did%3Abtcr2%3Ak1qq...",
|
|
301
|
-
* "controller": "did:btcr2:k1qq...",
|
|
302
|
-
* "invocationTarget": "did:btcr2:k1qq..."
|
|
303
|
-
* }
|
|
304
|
-
* ```
|
|
305
|
-
*/
|
|
306
|
-
export interface DidBtcr2RootCapability {
|
|
307
|
-
'@context': string | string[]; // e.g. "https://w3id.org/zcap/v1"
|
|
308
|
-
id: string; // e.g. "urn:zcap:root:<urlencoded-did>"
|
|
309
|
-
controller: string; // the DID
|
|
310
|
-
invocationTarget: string; // same as DID
|
|
311
|
-
}
|
package/src/types.ts
CHANGED
package/src/utils/date.ts
CHANGED
|
@@ -9,7 +9,7 @@ export class DateUtils {
|
|
|
9
9
|
* @param {Date} [date=new Date()] - The date to format.
|
|
10
10
|
* @returns {string} The formatted date string.
|
|
11
11
|
*/
|
|
12
|
-
static
|
|
12
|
+
static toISOStringNonFractional(date: Date = new Date()): string {
|
|
13
13
|
const time = date.getTime();
|
|
14
14
|
if (Number.isNaN(time)) {
|
|
15
15
|
throw new Error(`Invalid date: ${date}`);
|
|
@@ -29,4 +29,102 @@ export class DateUtils {
|
|
|
29
29
|
}
|
|
30
30
|
return Math.floor(date.getTime() / 1000);
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate if a string is a valid UTC date string.
|
|
35
|
+
* @param {string} dateString - The date string to validate.
|
|
36
|
+
* @returns {boolean} True if valid, otherwise false.
|
|
37
|
+
* @throws {Error} If the date string is invalid.
|
|
38
|
+
*/
|
|
39
|
+
static dateStringToTimestamp(dateString: string): Date {
|
|
40
|
+
const date = new Date(dateString);
|
|
41
|
+
if (Number.isNaN(date.getTime())) {
|
|
42
|
+
return new Date(0);
|
|
43
|
+
}
|
|
44
|
+
return date;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert a blocktime (Unix timestamp in seconds) to a Date object.
|
|
49
|
+
* @param {number} blocktime - The blocktime in seconds.
|
|
50
|
+
* @returns {Date} The corresponding Date object.
|
|
51
|
+
*/
|
|
52
|
+
static blocktimeToTimestamp(blocktime: number): Date {
|
|
53
|
+
return new Date(blocktime * 1000);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validates an XMLSCHEMA11-2 dateTime string.
|
|
58
|
+
* Format: [-]YYYY-MM-DDThh:mm:ss[.fractional][Z|(+|-)hh:mm]
|
|
59
|
+
*
|
|
60
|
+
* @see https://www.w3.org/TR/xmlschema11-2/#dateTime
|
|
61
|
+
*/
|
|
62
|
+
static isValidXsdDateTime(value?: string): boolean {
|
|
63
|
+
// Empty or undefined value is not a valid dateTime
|
|
64
|
+
if(!value) return false;
|
|
65
|
+
|
|
66
|
+
// Regex for XML Schema dateTime:
|
|
67
|
+
// - Optional leading minus for BCE years
|
|
68
|
+
// - Year: 4+ digits
|
|
69
|
+
// - Month: 01-12
|
|
70
|
+
// - Day: 01-31 (further validated below)
|
|
71
|
+
// - T separator
|
|
72
|
+
// - Hour: 00-23 (24:00:00 is valid end-of-day per spec)
|
|
73
|
+
// - Minute: 00-59
|
|
74
|
+
// - Second: 00-59 (with optional fractional part)
|
|
75
|
+
// - Timezone: Z or (+|-)hh:mm
|
|
76
|
+
const xsdDateTimeRegex =
|
|
77
|
+
/^-?(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
78
|
+
|
|
79
|
+
const match = value.match(xsdDateTimeRegex);
|
|
80
|
+
if (!match) return false;
|
|
81
|
+
|
|
82
|
+
const [, y, m, d, H, M, S, , tz] = match;
|
|
83
|
+
|
|
84
|
+
const year = parseInt(y, 10);
|
|
85
|
+
const month = parseInt(m, 10);
|
|
86
|
+
const day = parseInt(d, 10);
|
|
87
|
+
const hour = parseInt(H, 10);
|
|
88
|
+
const minute = parseInt(M, 10);
|
|
89
|
+
const second = parseInt(S, 10);
|
|
90
|
+
|
|
91
|
+
// Year 0000 is not valid in XML Schema (no year zero)
|
|
92
|
+
if (year === 0) return false;
|
|
93
|
+
|
|
94
|
+
// Month: 1-12
|
|
95
|
+
if (month < 1 || month > 12) return false;
|
|
96
|
+
|
|
97
|
+
// Day: validate against month (and leap year for February)
|
|
98
|
+
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
99
|
+
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
|
100
|
+
const maxDay = month === 2 && isLeapYear ? 29 : daysInMonth[month - 1];
|
|
101
|
+
if (day < 1 || day > maxDay) return false;
|
|
102
|
+
|
|
103
|
+
// Hour: 00-23, or 24:00:00 exactly (end-of-day)
|
|
104
|
+
if (hour === 24) {
|
|
105
|
+
if (minute !== 0 || second !== 0) return false;
|
|
106
|
+
} else if (hour > 23) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Minute: 00-59
|
|
111
|
+
if (minute > 59) return false;
|
|
112
|
+
|
|
113
|
+
// Second: 00-59 (leap second 60 is debatable; XML Schema doesn't explicitly allow it)
|
|
114
|
+
if (second > 59) return false;
|
|
115
|
+
|
|
116
|
+
// Validate timezone offset if present
|
|
117
|
+
if (tz && tz !== 'Z') {
|
|
118
|
+
const tzMatch = tz.match(/^[+-](\d{2}):(\d{2})$/);
|
|
119
|
+
if (tzMatch) {
|
|
120
|
+
const tzHour = parseInt(tzMatch[1], 10);
|
|
121
|
+
const tzMin = parseInt(tzMatch[2], 10);
|
|
122
|
+
if (tzHour > 14 || (tzHour === 14 && tzMin !== 0) || tzMin > 59) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
32
130
|
}
|