@@ -19,6 +19,10 @@ const secretArgWarning = 'Warning: passing the secret in argv may leak via shell
1919const passphraseArgWarning = 'Warning: passing the passphrase in argv may leak via shell history or process listings.\n' ;
2020const textEncoder = new TextEncoder ( ) ;
2121const textDecoder = new TextDecoder ( ) ;
22+ const secondsPerHour = 60 * 60 ;
23+ const secondsPerDay = secondsPerHour * 24 ;
24+ const maxExpiresInSeconds = 30 * secondsPerDay ;
25+ const defaultExpiresInSeconds = ProtocolConstants . defaultDuration * secondsPerDay ;
2226
2327function write ( stream , text ) {
2428 if ( text ) {
@@ -30,9 +34,9 @@ export function getHelpText() {
3034 return `1time v0
3135
3236Usage:
33- 1time send [--host <host-or-origin>] [secret]
37+ 1time send [--host <host-or-origin>] [--expires-in <Nd|Nh|NdNh>] [ secret]
3438 1time read [--host <host-or-origin>] <link>
35- 1time send-file [--host <host-or-origin>] [--passphrase <passphrase>] <path>
39+ 1time send-file [--host <host-or-origin>] [--expires-in <Nd|Nh|NdNh>] [-- passphrase <passphrase>] <path>
3640 1time read-file [--host <host-or-origin>] [--passphrase <passphrase>] [--out <path>] <link>
3741 1time --help
3842
@@ -42,9 +46,8 @@ Input precedence for send:
4246 3. positional secret argument (warns because argv is not safe)
4347
4448Notes:
45- - read only supports links passed as an argument in v0
4649 - send-file/read-file support optional passphrases via --passphrase or 1TIME_PASSPHRASE
47- - custom expiry is not supported in v0
50+ - --expires-in supports h and d units, for example 23h, 2d, or 2d23h (default 1d, max 30d)
4851 - http:// is only allowed for loopback hosts such as 127.0.0.1
4952` ;
5053}
@@ -91,6 +94,30 @@ function resolveOptionalPassphrase({env, values, stderr}) {
9194 return '' ;
9295}
9396
97+ function parseExpiresIn ( value ) {
98+ if ( value === undefined ) {
99+ return defaultExpiresInSeconds ;
100+ }
101+
102+ const raw = String ( value ) . trim ( ) . toLowerCase ( ) ;
103+ const match = / ^ (?: ( \d + ) d ) ? (?: ( \d + ) h ) ? $ / . exec ( raw ) ;
104+ if ( ! match || ( ! match [ 1 ] && ! match [ 2 ] ) ) {
105+ throw new Error ( `Invalid expires-in value "${ raw } ": use d and h units, for example 23h, 2d, or 2d23h.` ) ;
106+ }
107+
108+ const totalSeconds = Number ( match [ 1 ] || 0 ) * secondsPerDay + Number ( match [ 2 ] || 0 ) * secondsPerHour ;
109+
110+ if ( totalSeconds <= 0 ) {
111+ throw new Error ( `Invalid expires-in value "${ raw } ": duration must be greater than 0.` ) ;
112+ }
113+
114+ if ( totalSeconds > maxExpiresInSeconds ) {
115+ throw new Error ( `Invalid expires-in value "${ raw } ": maximum is 30d.` ) ;
116+ }
117+
118+ return totalSeconds ;
119+ }
120+
94121async function postJson ( { origin, path, payload, fetchImpl} ) {
95122 const response = await fetchImpl ( buildApiUrl ( origin , path ) , {
96123 method : 'POST' ,
@@ -107,7 +134,7 @@ async function postJson({origin, path, payload, fetchImpl}) {
107134 return response . json ( ) ;
108135}
109136
110- export async function createSecretLink ( { host, secret, fetchImpl} ) {
137+ export async function createSecretLink ( { host, secret, expiresInSeconds = defaultExpiresInSeconds , fetchImpl} ) {
111138 const origin = normalizeOrigin ( host || ProtocolConstants . defaultHost ) ;
112139 const generatedKey = getRandomString ( ProtocolConstants . randomKeyLen ) ;
113140 //left for later passphrase
@@ -118,7 +145,7 @@ export async function createSecretLink({host, secret, fetchImpl}) {
118145 payload : {
119146 secretMessage : encryptedMessage ,
120147 hashedKey,
121- duration : ProtocolConstants . defaultDuration * 86400 ,
148+ duration : expiresInSeconds ,
122149 } ,
123150 fetchImpl,
124151 } ) ;
@@ -188,7 +215,7 @@ async function writeFileToAvailablePath(targetPath, fileBytes) {
188215 throw new Error ( `Failed to allocate output path for ${ targetPath } ` ) ;
189216}
190217
191- async function createFileLink ( { host, filePath, passphrase = '' , fetchImpl} ) {
218+ async function createFileLink ( { host, filePath, passphrase = '' , expiresInSeconds = defaultExpiresInSeconds , fetchImpl} ) {
192219 const origin = normalizeOrigin ( host || ProtocolConstants . defaultHost ) ;
193220 const fileBytes = await readFile ( filePath ) ;
194221 const meta = {
@@ -204,7 +231,7 @@ async function createFileLink({host, filePath, passphrase = '', fetchImpl}) {
204231 const formData = new FormData ( ) ;
205232 formData . append ( 'file' , new Blob ( [ encryptedBytes ] ) , 'encrypted.bin' ) ;
206233 formData . append ( 'hashedKey' , hashedKey ) ;
207- formData . append ( 'duration' , String ( ProtocolConstants . defaultDuration * 86400 ) ) ;
234+ formData . append ( 'duration' , String ( expiresInSeconds ) ) ;
208235
209236 const response = await fetchImpl ( buildApiUrl ( origin , 'saveFile' ) , {
210237 method : 'POST' ,
@@ -322,6 +349,9 @@ function parseSendArgs(args) {
322349 host : {
323350 type : 'string' ,
324351 } ,
352+ 'expires-in' : {
353+ type : 'string' ,
354+ } ,
325355 } ,
326356 } ) ;
327357}
@@ -338,6 +368,9 @@ function parseSendFileArgs(args) {
338368 host : {
339369 type : 'string' ,
340370 } ,
371+ 'expires-in' : {
372+ type : 'string' ,
373+ } ,
341374 passphrase : {
342375 type : 'string' ,
343376 } ,
@@ -414,6 +447,7 @@ export async function run(argv = process.argv.slice(2), io = {}) {
414447 }
415448
416449 try {
450+ const expiresInSeconds = parseExpiresIn ( values [ 'expires-in' ] ) ;
417451 const secret = await resolveSecret ( {
418452 stdin,
419453 env,
@@ -426,6 +460,7 @@ export async function run(argv = process.argv.slice(2), io = {}) {
426460 const link = await createSecretLink ( {
427461 host : values . host ,
428462 secret,
463+ expiresInSeconds,
429464 fetchImpl,
430465 } ) ;
431466 write ( stdout , `${ link } \n` ) ;
@@ -448,10 +483,12 @@ export async function run(argv = process.argv.slice(2), io = {}) {
448483 }
449484
450485 try {
486+ const expiresInSeconds = parseExpiresIn ( values [ 'expires-in' ] ) ;
451487 const link = await createFileLink ( {
452488 host : values . host ,
453489 filePath : positionals [ 0 ] ,
454490 passphrase : resolveOptionalPassphrase ( { env, values, stderr} ) ,
491+ expiresInSeconds,
455492 fetchImpl,
456493 } ) ;
457494 write ( stdout , `${ link } \n` ) ;
0 commit comments