From 6c7ef1ee98f167ec5c5c8f4bc14212b7d8e75905 Mon Sep 17 00:00:00 2001 From: Bnonni Date: Tue, 30 Apr 2024 10:07:53 -0400 Subject: [PATCH 1/5] fetch and merge upsteam changes --- src/core/dwn-error.ts | 2 ++ src/core/protocol-authorization.ts | 28 +++++++++++++++++++++++++++ src/interfaces/protocols-configure.ts | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 5cab8075f..c01ade875 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', + ProtocolAuthorizationExpiryReached = 'ProtocolAuthorizationExpiryReached', ProtocolAuthorizationIncorrectDataFormat = 'ProtocolAuthorizationIncorrectDataFormat', ProtocolAuthorizationIncorrectContextId = 'ProtocolAuthorizationIncorrectContextId', ProtocolAuthorizationIncorrectProtocolPath = 'ProtocolAuthorizationIncorrectProtocolPath', @@ -75,6 +76,7 @@ export enum DwnErrorCode { ProtocolAuthorizationTagsInvalidSchema = 'ProtocolAuthorizationTagsInvalidSchema', ProtocolsConfigureDuplicateActorInRuleSet = 'ProtocolsConfigureDuplicateActorInRuleSet', ProtocolsConfigureDuplicateRoleInRuleSet = 'ProtocolsConfigureDuplicateRoleInRuleSet', + ProtocolsConfigureInvalidExpiry = 'ProtocolsConfigureInvalidExpiry', ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize', ProtocolsConfigureInvalidActionMissingOf = 'ProtocolsConfigureInvalidActionMissingOf', ProtocolsConfigureInvalidActionOfNotAllowed = 'ProtocolsConfigureInvalidActionOfNotAllowed', diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index 8d4c3a554..e2aa6b732 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.verifyExpiry(incomingMessage, ruleSet) } public static async authorizeQueryOrSubscribe( @@ -726,6 +729,31 @@ export class ProtocolAuthorization { } } + /** + * Verifies that reads adhere to the $expiry constraint if provided + * @throws {Error} if expiry date is passed. + */ + private static verifyExpiry( + incomingMessage: RecordsRead, + ruleSet: ProtocolRuleSet + ): void { + const ruleExpiry = ruleSet.$expiry; + if (!ruleExpiry) { + return; + } + + const dateCreated = incomingMessage.message.descriptor.filter?.dateCreated; + if (!dateCreated) { + return; + } + + const dateExpiry = dateCreated + ruleExpiry; + if (Date.now() > dateExpiry) { + throw new DwnError(DwnErrorCode.ProtocolAuthorizationExpiryReached, `dateExpiry ${dateExpiry} has passed`); + } + + } + /** * 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 Date: Wed, 24 Apr 2024 12:33:35 -0400 Subject: [PATCH 2/5] rename epxiry to expiration; update type number to object --- .../interface-methods/protocol-rule-set.json | 66 ++++++++++++++----- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/json-schemas/interface-methods/protocol-rule-set.json b/json-schemas/interface-methods/protocol-rule-set.json index 4e8309648..0d511da5e 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,16 +167,32 @@ } }, "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": { + "duration": { + "type": "number", + "minimum": 1, + "$comment": "Amount of milliseconds to add to dateCreated to determine expiration date" + }, + "timespan": { + "type": "string", + "minimum": "1s", + "$comment": "Time span to add to dateCreated to determine expiration date (e.g. 30s, 45m, 10h, 2D, 3W, 6M, 2Y)" + }, + "datetime": { + "type": "string", + "$comment": "Datetime string in ISO8601 format (YYYY-MM-DDThh:mm:ssZ) after which a record expires" + } } } + }, + "patternProperties": { + "^[^$].*$": { + "$ref": "https://identity.foundation/dwn/json-schemas/protocol-rule-set.json" + } } - } - } - }, - "patternProperties": { - "^[^$].*$": { - "$ref": "https://identity.foundation/dwn/json-schemas/protocol-rule-set.json" - } - } -} \ No newline at end of file + } \ No newline at end of file From 84fdcf482eb5be60f2799f1c8356901d6298be96 Mon Sep 17 00:00:00 2001 From: Bnonni Date: Fri, 26 Apr 2024 20:20:33 -0400 Subject: [PATCH 3/5] pushing latest changes for expiration --- src/core/dwn-error.ts | 6 ++-- src/core/protocol-authorization.ts | 49 +++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index c01ade875..f05f6f378 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -55,7 +55,7 @@ export enum DwnErrorCode { PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve', ProtocolAuthorizationActionNotAllowed = 'ProtocolAuthorizationActionNotAllowed', ProtocolAuthorizationActionRulesNotFound = 'ProtocolAuthorizationActionRulesNotFound', - ProtocolAuthorizationExpiryReached = 'ProtocolAuthorizationExpiryReached', + ProtocolAuthorizationExpirationPassed = 'ProtocolAuthorizationExpirationPassed', ProtocolAuthorizationIncorrectDataFormat = 'ProtocolAuthorizationIncorrectDataFormat', ProtocolAuthorizationIncorrectContextId = 'ProtocolAuthorizationIncorrectContextId', ProtocolAuthorizationIncorrectProtocolPath = 'ProtocolAuthorizationIncorrectProtocolPath', @@ -76,7 +76,9 @@ export enum DwnErrorCode { ProtocolAuthorizationTagsInvalidSchema = 'ProtocolAuthorizationTagsInvalidSchema', ProtocolsConfigureDuplicateActorInRuleSet = 'ProtocolsConfigureDuplicateActorInRuleSet', ProtocolsConfigureDuplicateRoleInRuleSet = 'ProtocolsConfigureDuplicateRoleInRuleSet', - ProtocolsConfigureInvalidExpiry = 'ProtocolsConfigureInvalidExpiry', + ProtocolsConfigureInvalidExpiration = 'ProtocolsConfigureInvalidExpiration', + ProtocolsConfigureInvalidExpirationDuration = 'ProtocolsConfigureInvalidExpirationDuration', + ProtocolsConfigureInvalidExpirationDatetime = 'ProtocolsConfigureInvalidExpirationDatetime', ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize', ProtocolsConfigureInvalidActionMissingOf = 'ProtocolsConfigureInvalidActionMissingOf', ProtocolsConfigureInvalidActionOfNotAllowed = 'ProtocolsConfigureInvalidActionOfNotAllowed', diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index e2aa6b732..e54ec9cd8 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -163,7 +163,7 @@ export class ProtocolAuthorization { ); // Verify expiry - ProtocolAuthorization.verifyExpiry(incomingMessage, ruleSet) + ProtocolAuthorization.verifyExpiration(incomingMessage, ruleSet); } public static async authorizeQueryOrSubscribe( @@ -730,29 +730,48 @@ export class ProtocolAuthorization { } /** - * Verifies that reads adhere to the $expiry constraint if provided - * @throws {Error} if expiry date is passed. - */ - private static verifyExpiry( - incomingMessage: RecordsRead, + * Verifies that queries and reads adhere to the $expiration constraint if provided + * @throws {Error} if more than one $expiration property is set, + */ + private static verifyExpiration( + incomingMessage: RecordsWrite, ruleSet: ProtocolRuleSet ): void { - const ruleExpiry = ruleSet.$expiry; - if (!ruleExpiry) { + const ruleExpiration = ruleSet.$expiration; + if (!ruleExpiration) { return; } - const dateCreated = incomingMessage.message.descriptor.filter?.dateCreated; - if (!dateCreated) { - return; + const expirationEntries = Object.entries(ruleExpiration); + if (expirationEntries.length > 1) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpiration, + `invalid property: expiration ${ruleExpiration} cannot set more than one property`); } - const dateExpiry = dateCreated + ruleExpiry; - if (Date.now() > dateExpiry) { - throw new DwnError(DwnErrorCode.ProtocolAuthorizationExpiryReached, `dateExpiry ${dateExpiry} has passed`); + const { duration, datetime } = ruleExpiration; + if (!duration && !datetime) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpiration, `invalid property ${ruleExpiration}: must set at least one property`); } - } + if (duration && typeof duration === 'number' && duration < 1) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, `invalid property: duration ${duration} number must must be >= 1`); + } else if (typeof duration === 'string') { + const [amount, unit] = duration.split('')[0]; + if ((unit === 's' && parseInt(amount) < 1)) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, + `invalid property: if duration unit ${unit} = s, amount ${amount} must >= 1 (i.e. 1s)`); + } + if (!/\d{1,}(s|m|h|d|y)/.test(duration)) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, + `invalid property: duration ${duration} format must be \"\" (e.g. 1s, 10d, 5y)`); + } + } + + if (datetime && /(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)Z/.test(datetime)) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDatetime, + `invalid format: datetime ${datetime} does not conform to ISO-8601 UTC format`); + } + }; /** * If the given RecordsWrite is not a role record, this method does nothing and succeeds immediately. From 7d19e520fb6cedde68e38f19e0d5bfe863d776f5 Mon Sep 17 00:00:00 2001 From: Bnonni Date: Wed, 1 May 2024 14:27:34 -0400 Subject: [PATCH 4/5] reworking based on feedback --- .../interface-methods/protocol-rule-set.json | 61 ++++++++++++------- src/core/dwn-error.ts | 1 - src/core/protocol-authorization.ts | 47 +++++++------- src/types/protocols-types.ts | 4 ++ src/types/records-types.ts | 11 ++++ 5 files changed, 78 insertions(+), 46 deletions(-) diff --git a/json-schemas/interface-methods/protocol-rule-set.json b/json-schemas/interface-methods/protocol-rule-set.json index 0d511da5e..125ed0e20 100644 --- a/json-schemas/interface-methods/protocol-rule-set.json +++ b/json-schemas/interface-methods/protocol-rule-set.json @@ -168,31 +168,46 @@ }, "patternProperties": { "^(enum|minimum|maximum|exclusiveMinimum|exclusiveMaximum|minLength|maxLength|minItems|maxItems|uniqueItems|minContains|maxContains)$": {} - }, - "$expiration": { - "type": "object", - "additionalProperties": false, - "properties": { - "duration": { + } + } + } + }, + "$expiration": { + "type": "object", + "additionalProperties": false, + "properties": { + "duration": { + "type": "object", + "properties": { + "oneOf": [ + { + "type": "object", + "$comment": "Amount and unit represent a semantic duration of time using an amount (number) and a unit (character) to set an expiration date relative to record.dateCreated (e.g. 1s). Upon record query/read, this is converted to milliseconds, added to record.dateCreated to calculate dateExpiration and compared against original dateCreated.", + "properties": { + "amount": { + "type": "number", + "$comment": "" + }, + "unit": { + "type": "string", + "$comment": "Single char representing a unit of time. Must be one of: s (seconds), m (minutes), h (hours), d (days), y (years). E.g. 100s, 75m, 2d, 4y" + } + } + }, + { "type": "number", "minimum": 1, - "$comment": "Amount of milliseconds to add to dateCreated to determine expiration date" - }, - "timespan": { - "type": "string", - "minimum": "1s", - "$comment": "Time span to add to dateCreated to determine expiration date (e.g. 30s, 45m, 10h, 2D, 3W, 6M, 2Y)" - }, - "datetime": { - "type": "string", - "$comment": "Datetime string in ISO8601 format (YYYY-MM-DDThh:mm:ssZ) after which a record expires" + "$comment": "Numeric duration representing amount of milliseconds to wait from the record.dateCreated before records expire" } - } - } - }, - "patternProperties": { - "^[^$].*$": { - "$ref": "https://identity.foundation/dwn/json-schemas/protocol-rule-set.json" + ] } } - } \ No newline at end of file + } + } + }, + "patternProperties": { + "^[^$].*$": { + "$ref": "https://identity.foundation/dwn/json-schemas/protocol-rule-set.json" + } + } +} \ No newline at end of file diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index f05f6f378..97879b882 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -78,7 +78,6 @@ export enum DwnErrorCode { ProtocolsConfigureDuplicateRoleInRuleSet = 'ProtocolsConfigureDuplicateRoleInRuleSet', ProtocolsConfigureInvalidExpiration = 'ProtocolsConfigureInvalidExpiration', ProtocolsConfigureInvalidExpirationDuration = 'ProtocolsConfigureInvalidExpirationDuration', - ProtocolsConfigureInvalidExpirationDatetime = 'ProtocolsConfigureInvalidExpirationDatetime', ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize', ProtocolsConfigureInvalidActionMissingOf = 'ProtocolsConfigureInvalidActionMissingOf', ProtocolsConfigureInvalidActionOfNotAllowed = 'ProtocolsConfigureInvalidActionOfNotAllowed', diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index e54ec9cd8..d10f1b2ca 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -737,40 +737,43 @@ export class ProtocolAuthorization { incomingMessage: RecordsWrite, ruleSet: ProtocolRuleSet ): void { - const ruleExpiration = ruleSet.$expiration; - if (!ruleExpiration) { + const { expiration } = incomingMessage.message.descriptor ?? {}; + if (!expiration){ return; } - const expirationEntries = Object.entries(ruleExpiration); - if (expirationEntries.length > 1) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpiration, - `invalid property: expiration ${ruleExpiration} cannot set more than one property`); + const { duration } = expiration ?? {}; + if (!duration) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpiration, `invalid property: duration cannot be null`); } - - const { duration, datetime } = ruleExpiration; - if (!duration && !datetime) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpiration, `invalid property ${ruleExpiration}: must set at least one property`); + const typeOfDuration: string | number = typeof duration; + if (!['string', 'number'].includes(typeOfDuration)) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, + `invalid property: duration must be string or number, not ${typeOfDuration}`); } - if (duration && typeof duration === 'number' && duration < 1) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, `invalid property: duration ${duration} number must must be >= 1`); - } else if (typeof duration === 'string') { - const [amount, unit] = duration.split('')[0]; - if ((unit === 's' && parseInt(amount) < 1)) { + if (typeof duration === 'number' && duration < 1) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, + `invalid property: duration ${duration} number must must be >= 1`); + } else if (typeof duration === 'string'){ + const units = ['s', 'm', 'h', 'd', 'y']; + if (!units.some(unit => duration.endsWith(unit))) { throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, - `invalid property: if duration unit ${unit} = s, amount ${amount} must >= 1 (i.e. 1s)`); + `invalid property: duration ${duration} must end with one of ${units}`); } - if (!/\d{1,}(s|m|h|d|y)/.test(duration)) { + + if (/^0/.test(duration)) { throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, - `invalid property: duration ${duration} format must be \"\" (e.g. 1s, 10d, 5y)`); + `invalid property: duration cannot start with 0`); } - } - if (datetime && /(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)Z/.test(datetime)) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDatetime, - `invalid format: datetime ${datetime} does not conform to ISO-8601 UTC format`); + if (isNaN(parseInt(duration.slice(0, -1)))) { + throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, + `invalid property: duration cannot be NaN`); + } } + + }; /** diff --git a/src/types/protocols-types.ts b/src/types/protocols-types.ts index d536638c9..02ecd7c3b 100644 --- a/src/types/protocols-types.ts +++ b/src/types/protocols-types.ts @@ -140,6 +140,10 @@ export type ProtocolRuleSet = { [key: string]: any; } + $expiration?: { + duration?: string | number; + } + // JSON Schema verifies that properties other than properties prefixed with $ will actually have type ProtocolRuleSet [key: string]: any; }; diff --git a/src/types/records-types.ts b/src/types/records-types.ts index 340d60378..d49920b59 100644 --- a/src/types/records-types.ts +++ b/src/types/records-types.ts @@ -21,6 +21,15 @@ export type RecordsWriteTags = { export type RecordsWriteTagsFilter = StartsWithFilter | RangeFilter | string | number | boolean; +export type ExpirationFilter = { + duration?: string | number; +}; + +export type RecordsWriteExpiration = { + [property: string]: string | number; +}; + + export type RecordsWriteDescriptor = { interface: DwnInterfaceName.Records; method: DwnMethodName.Write; @@ -32,6 +41,7 @@ export type RecordsWriteDescriptor = { parentId?: string; dataCid: string; dataSize: number; + expiration?: RecordsWriteExpiration; dateCreated: string; messageTimestamp: string; published?: boolean; @@ -152,6 +162,7 @@ export type RecordsFilter = { parentId?: string; dataFormat?: string; dataSize?: RangeFilter; + expiration?: ExpirationFilter; dataCid?: string; dateCreated?: RangeCriterion; datePublished?: RangeCriterion; From e99e72ad2668be08b86bc532f299a617539a707e Mon Sep 17 00:00:00 2001 From: Bnonni Date: Wed, 29 May 2024 14:10:28 -0400 Subject: [PATCH 5/5] pushing latest to set pause point for picking backup --- .../interface-methods/protocol-rule-set.json | 41 ++++++----- src/core/dwn-error.ts | 6 +- src/core/protocol-authorization.ts | 68 +++++++++++-------- src/types/protocols-types.ts | 4 +- src/types/records-types.ts | 4 +- 5 files changed, 66 insertions(+), 57 deletions(-) diff --git a/json-schemas/interface-methods/protocol-rule-set.json b/json-schemas/interface-methods/protocol-rule-set.json index 125ed0e20..ca00717ab 100644 --- a/json-schemas/interface-methods/protocol-rule-set.json +++ b/json-schemas/interface-methods/protocol-rule-set.json @@ -176,32 +176,29 @@ "type": "object", "additionalProperties": false, "properties": { - "duration": { - "type": "object", - "properties": { - "oneOf": [ - { - "type": "object", - "$comment": "Amount and unit represent a semantic duration of time using an amount (number) and a unit (character) to set an expiration date relative to record.dateCreated (e.g. 1s). Upon record query/read, this is converted to milliseconds, added to record.dateCreated to calculate dateExpiration and compared against original dateCreated.", - "properties": { - "amount": { - "type": "number", - "$comment": "" - }, - "unit": { - "type": "string", - "$comment": "Single char representing a unit of time. Must be one of: s (seconds), m (minutes), h (hours), d (days), y (years). E.g. 100s, 75m, 2d, 4y" - } - } - }, - { + "oneOf": [ + { + "required": [ + "amount" + ], + "type": "object", + "properties": { + "amount": { "type": "number", "minimum": 1, - "$comment": "Numeric duration representing amount of milliseconds to wait from the record.dateCreated before records expire" + "$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`." } - } + ] } } }, diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 97879b882..b182bdb0c 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -74,10 +74,12 @@ export enum DwnErrorCode { ProtocolAuthorizationQueryWithoutRole = 'ProtocolAuthorizationQueryWithoutRole', ProtocolAuthorizationRoleMissingRecipient = 'ProtocolAuthorizationRoleMissingRecipient', ProtocolAuthorizationTagsInvalidSchema = 'ProtocolAuthorizationTagsInvalidSchema', + ProtocolsAuthorizationExpirationMissing = 'ProtocolsAuthorizationExpirationMissing', + ProtocolsAuthorizationExpirationInvalid = 'ProtocolsAuthorizationExpirationInvalid', + ProtocolsAuthorizationExpirationAmountInvalid = 'ProtocolsAuthorizationExpirationAmountInvalid', + ProtocolsAuthorizationExpirationUnitInvalid = 'ProtocolsAuthorizationExpirationUnitInvalid', ProtocolsConfigureDuplicateActorInRuleSet = 'ProtocolsConfigureDuplicateActorInRuleSet', ProtocolsConfigureDuplicateRoleInRuleSet = 'ProtocolsConfigureDuplicateRoleInRuleSet', - ProtocolsConfigureInvalidExpiration = 'ProtocolsConfigureInvalidExpiration', - ProtocolsConfigureInvalidExpirationDuration = 'ProtocolsConfigureInvalidExpirationDuration', ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize', ProtocolsConfigureInvalidActionMissingOf = 'ProtocolsConfigureInvalidActionMissingOf', ProtocolsConfigureInvalidActionOfNotAllowed = 'ProtocolsConfigureInvalidActionOfNotAllowed', diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index d10f1b2ca..117e51601 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -731,48 +731,62 @@ export class ProtocolAuthorization { /** * Verifies that queries and reads adhere to the $expiration constraint if provided - * @throws {Error} if more than one $expiration property is set, + * @throws {Error} if typeof $expiration != number | {} + * @throws {Error} if typeof $expiration === {} and amount | unit === undefined + * */ private static verifyExpiration( incomingMessage: RecordsWrite, ruleSet: ProtocolRuleSet ): void { - const { expiration } = incomingMessage.message.descriptor ?? {}; - if (!expiration){ + const ruleExpiration = ruleSet?.$expiration; + if (!ruleExpiration) { return; } - const { duration } = expiration ?? {}; - if (!duration) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpiration, `invalid property: duration cannot be null`); + const incomingExpiration = incomingMessage.message.descriptor.expiration; + if (!incomingExpiration) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationMissing, + `missing expiration descriptor: protocol ruleset requires $expiration`); } - const typeOfDuration: string | number = typeof duration; - if (!['string', 'number'].includes(typeOfDuration)) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, - `invalid property: duration must be string or number, not ${typeOfDuration}`); + + 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 (typeof duration === 'number' && duration < 1) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, - `invalid property: duration ${duration} number must must be >= 1`); - } else if (typeof duration === 'string'){ - const units = ['s', 'm', 'h', 'd', 'y']; - if (!units.some(unit => duration.endsWith(unit))) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, - `invalid property: duration ${duration} must end with one of ${units}`); - } + if (!amount) { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationAmountInvalid, + `invalid expiration descriptor: if set, $expiration.amount ${amount} cannot be null`); + } - if (/^0/.test(duration)) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, - `invalid property: duration cannot start with 0`); - } + if (typeof amount !== 'number') { + throw new DwnError(DwnErrorCode.ProtocolsAuthorizationExpirationAmountInvalid, + `invalid expiration property: $expiration.amount ${amount} type = ${typeof amount}, must be number`); + } - if (isNaN(parseInt(duration.slice(0, -1)))) { - throw new DwnError(DwnErrorCode.ProtocolsConfigureInvalidExpirationDuration, - `invalid property: duration cannot be NaN`); - } + 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()}`); + } }; diff --git a/src/types/protocols-types.ts b/src/types/protocols-types.ts index 02ecd7c3b..c95f26cdc 100644 --- a/src/types/protocols-types.ts +++ b/src/types/protocols-types.ts @@ -140,9 +140,7 @@ export type ProtocolRuleSet = { [key: string]: any; } - $expiration?: { - duration?: string | number; - } + $expiration?: { amount: number, unit: string }; // JSON Schema verifies that properties other than properties prefixed with $ will actually have type ProtocolRuleSet [key: string]: any; diff --git a/src/types/records-types.ts b/src/types/records-types.ts index d49920b59..c124cfc88 100644 --- a/src/types/records-types.ts +++ b/src/types/records-types.ts @@ -25,9 +25,7 @@ export type ExpirationFilter = { duration?: string | number; }; -export type RecordsWriteExpiration = { - [property: string]: string | number; -}; +export type RecordsWriteExpiration = { amount: number; unit: string }; export type RecordsWriteDescriptor = {