diff --git a/json-schemas/interface-methods/protocol-rule-set.json b/json-schemas/interface-methods/protocol-rule-set.json index 4e8309648..ca00717ab 100644 --- a/json-schemas/interface-methods/protocol-rule-set.json +++ b/json-schemas/interface-methods/protocol-rule-set.json @@ -112,13 +112,13 @@ "minProperties": 1, "properties": { "$requiredTags": { - "type": "array", - "items": { - "type": "string" - } + "type": "array", + "items": { + "type": "string" + } }, "$allowUndefinedTags": { - "type": "boolean" + "type": "boolean" } }, "patternProperties": { @@ -127,13 +127,23 @@ "additionalProperties": false, "properties": { "type": { - "enum": ["string", "number", "integer", "boolean", "array"] + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] }, "items": { "type": "object", "properties": { "type": { - "enum": ["string", "number", "integer"] + "enum": [ + "string", + "number", + "integer" + ] } }, "patternProperties": { @@ -144,7 +154,11 @@ "type": "object", "properties": { "type": { - "enum": ["string", "number", "integer"] + "enum": [ + "string", + "number", + "integer" + ] } }, "patternProperties": { @@ -153,11 +167,39 @@ } }, "patternProperties": { - "^(enum|minimum|maximum|exclusiveMinimum|exclusiveMaximum|minLength|maxLength|minItems|maxItems|uniqueItems|minContains|maxContains)$": { - } + "^(enum|minimum|maximum|exclusiveMinimum|exclusiveMaximum|minLength|maxLength|minItems|maxItems|uniqueItems|minContains|maxContains)$": {} } } } + }, + "$expiration": { + "type": "object", + "additionalProperties": false, + "properties": { + "oneOf": [ + { + "required": [ + "amount" + ], + "type": "object", + "properties": { + "amount": { + "type": "number", + "minimum": 1, + "$comment": "If only amount is set without unit, represents amount of milliseconds to wait from the record.dateCreated before records expire." + }, + "unit": { + "type": "string", + "$comment": "Single character signaling how to interpret the given 'amount'. If present, must be one of: s (seconds), m (minutes), h (hours), d (days), y (years). E.g. 100s, 75m, 2d, 4y", + "patternProperties": { + "^(s|m|d|y)$": {} + } + } + }, + "$comment": "Amount is used to calculate an expiration date. Amount and unit are used to calculate a millisecond duration, which is then used to calculate an expiration date. Expiration is relative to `record.dateCreated`." + } + ] + } } }, "patternProperties": { diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 5cab8075f..b182bdb0c 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -55,6 +55,7 @@ export enum DwnErrorCode { PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve', ProtocolAuthorizationActionNotAllowed = 'ProtocolAuthorizationActionNotAllowed', ProtocolAuthorizationActionRulesNotFound = 'ProtocolAuthorizationActionRulesNotFound', + ProtocolAuthorizationExpirationPassed = 'ProtocolAuthorizationExpirationPassed', ProtocolAuthorizationIncorrectDataFormat = 'ProtocolAuthorizationIncorrectDataFormat', ProtocolAuthorizationIncorrectContextId = 'ProtocolAuthorizationIncorrectContextId', ProtocolAuthorizationIncorrectProtocolPath = 'ProtocolAuthorizationIncorrectProtocolPath', @@ -73,6 +74,10 @@ export enum DwnErrorCode { ProtocolAuthorizationQueryWithoutRole = 'ProtocolAuthorizationQueryWithoutRole', ProtocolAuthorizationRoleMissingRecipient = 'ProtocolAuthorizationRoleMissingRecipient', ProtocolAuthorizationTagsInvalidSchema = 'ProtocolAuthorizationTagsInvalidSchema', + ProtocolsAuthorizationExpirationMissing = 'ProtocolsAuthorizationExpirationMissing', + ProtocolsAuthorizationExpirationInvalid = 'ProtocolsAuthorizationExpirationInvalid', + ProtocolsAuthorizationExpirationAmountInvalid = 'ProtocolsAuthorizationExpirationAmountInvalid', + ProtocolsAuthorizationExpirationUnitInvalid = 'ProtocolsAuthorizationExpirationUnitInvalid', ProtocolsConfigureDuplicateActorInRuleSet = 'ProtocolsConfigureDuplicateActorInRuleSet', ProtocolsConfigureDuplicateRoleInRuleSet = 'ProtocolsConfigureDuplicateRoleInRuleSet', ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize', diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index 8d4c3a554..117e51601 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -161,6 +161,9 @@ export class ProtocolAuthorization { ancestorMessageChain, messageStore, ); + + // Verify expiry + ProtocolAuthorization.verifyExpiration(incomingMessage, ruleSet); } public static async authorizeQueryOrSubscribe( @@ -726,6 +729,67 @@ export class ProtocolAuthorization { } } + /** + * Verifies that queries and reads adhere to the $expiration constraint if provided + * @throws {Error} if typeof $expiration != number | {} + * @throws {Error} if typeof $expiration === {} and amount | unit === undefined + * + */ + private static verifyExpiration( + incomingMessage: RecordsWrite, + ruleSet: ProtocolRuleSet + ): void { + const ruleExpiration = ruleSet?.$expiration; + if (!ruleExpiration) { + return; + } + + const incomingExpiration = incomingMessage.message.descriptor.expiration; + if (!incomingExpiration) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationMissing, + `missing expiration descriptor: protocol ruleset requires $expiration`); + } + + const { amount, unit } = incomingExpiration || {}; + if (!(amount && unit)) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationAmountInvalid, + `invalid expiration descriptor: if set, $expiration must be type object with properties amount and/or unit`); + } + + if (!amount) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationAmountInvalid, + `invalid expiration descriptor: if set, $expiration.amount ${amount} cannot be null`); + } + + if (typeof amount !== 'number') { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationAmountInvalid, + `invalid expiration property: $expiration.amount ${amount} type = ${typeof amount}, must be number`); + } + + if (isNaN(amount)) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationAmountInvalid, + `invalid expiration property: $expiration.amount ${amount} cannot be NaN`); + } + + if (amount <= 0) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationAmountInvalid, + `invalid expiration property: $expiration.amount ${amount} must be greater than 0`); + } + + if (typeof unit !== 'string') { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationUnitInvalid, + `invalid expiration property: $expiration.unit ${unit} type ${typeof unit} must be string`); + } + + + const validUnits = ['s', 'm', 'h', 'd', 'y']; + if (!validUnits.some(validUnit => unit === validUnit)) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationUnitInvalid, + `invalid property: expiration.unit ${unit} must be one of ${validUnits.join()}`); + } + + }; + /** * If the given RecordsWrite is not a role record, this method does nothing and succeeds immediately. * diff --git a/src/interfaces/protocols-configure.ts b/src/interfaces/protocols-configure.ts index 61fa569df..8af234da2 100644 --- a/src/interfaces/protocols-configure.ts +++ b/src/interfaces/protocols-configure.ts @@ -132,7 +132,7 @@ export class ProtocolsConfigure extends AbstractMessage